Fake Reader

在任意网页中阅读EPUB电子书,支持EPUB渲染模式和纯文本模式

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Fake Reader
// @namespace    
// @version      1.1.0
// @description  在任意网页中阅读EPUB电子书,支持EPUB渲染模式和纯文本模式
// @author       kudou61
// @match        *://*/*
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/epub.min.js
// ==/UserScript==

/**
 * Fake Reader - 在网页中阅读EPUB电子书的油猴脚本
 *
 * 功能特性:
 * - EPUB渲染模式:使用epub.js进行完整渲染
 * - 纯文本模式:提取文本内容,按章节或字符数分页
 * - 键盘翻页:← → 或 Cmd/Ctrl + [ ]
 * - 页码跳转:在设置面板输入页码
 * - 进度保存:自动保存阅读进度
 * - 快捷键:Cmd/Ctrl + B 打开设置面板
 */

(function() {
    'use strict';

    // ============================================================================
    // 常量配置
    // ============================================================================

    const CONSTANTS = {
        // DOM 元素 ID
        IDs: {
            OVERLAY: 'er-overlay',
            SETTINGS_PANEL: 'er-settings',
            READER: 'er-reader',
            TEXT_READER: 'er-text-reader',
            TEXT_DISPLAY: 'er-text-display',
            PAGE_CONTENT: 'er-page-content',
            STYLES: 'er-styles',
            READING_PROGRESS: 'er-reading-progress'
        },

        // 存储键
        STORAGE: {
            TARGET_DIV: 'epub_reader_target_div',
            BOOK_PROGRESS: 'epub_reader_progress'
        },

        // 默认配置
        CONFIG: {
            CHARS_PER_PAGE_MIN: 500,
            CHARS_PER_PAGE_MAX: 5000,
            EPUB_LOCATION_INTERVAL: 1600,
            NOTIFICATION_DURATION: 3000
        },

        // 命名空间(全局变量前缀)
        NS: 'epubReader'
    };

    // ============================================================================
    // 全局状态
    // ============================================================================

    const State = {
        // EPUB 相关
        book: null,
        rendition: null,
        currentBookFile: null,

        // 阅读器状态
        isReaderActive: false,
        settingsPanel: null,

        // 键盘事件清理函数
        keydownCleanup: null
    };

    // 暴露给全局的接口(供设置面板调用)
    window[CONSTANTS.NS + 'Interface'] = null;
    window[CONSTANTS.NS + 'State'] = null;

    // ============================================================================
    // 工具模块 (Utils)
    // ============================================================================

    const Utils = {
        /**
         * 读取文件为 ArrayBuffer
         * @param {File} file - 文件对象
         * @returns {Promise<ArrayBuffer>}
         */
        readFileAsArrayBuffer(file) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = (e) => resolve(e.target.result);
                reader.onerror = () => reject(new Error('文件读取失败'));
                reader.readAsArrayBuffer(file);
            });
        },

        /**
         * 显示通知消息
         * @param {string} message - 消息内容
         * @param {string} type - 类型: 'info' | 'error' | 'success'
         */
        showNotification(message, type = 'info') {
            const notification = document.createElement('div');
            notification.className = `${CONSTANTS.IDs.STYLES}-notification`;

            const colors = {
                info: { borderLeft: '#3b82f6', bg: '#eff6ff' },
                error: { borderLeft: '#ef4444', bg: '#fef2f2' },
                success: { borderLeft: '#22c55e', bg: '#f0fdf4' }
            };
            const color = colors[type] || colors.info;

            notification.style.cssText = `
                position: fixed;
                top: 24px;
                right: 24px;
                background: ${color.bg};
                color: #333;
                padding: 14px 20px;
                border-radius: 12px;
                z-index: 10001;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                font-size: 14px;
                font-weight: 500;
                box-shadow: 0 8px 24px rgba(0,0,0,0.15);
                max-width: 320px;
                border-left: 4px solid ${color.borderLeft};
                opacity: 0;
                transform: translateX(20px);
                animation: er-notification-in 0.3s ease forwards;
                transition: opacity 0.3s ease, transform 0.3s ease;
            `;

            notification.textContent = message;
            document.body.appendChild(notification);

            setTimeout(() => {
                notification.style.opacity = '0';
                notification.style.transform = 'translateX(20px)';
                setTimeout(() => notification.remove(), 300);
            }, CONSTANTS.CONFIG.NOTIFICATION_DURATION);
        },

        /**
         * 存储适配器
         */
        Storage: {
            set(key, value) {
                window.localStorage.setItem(key, JSON.stringify(value));
            },
            get(key, defaultValue = null) {
                try {
                    const item = window.localStorage.getItem(key);
                    return item ? JSON.parse(item) : defaultValue;
                } catch {
                    return defaultValue;
                }
            },
            remove(key) {
                window.localStorage.removeItem(key);
            }
        },

        /**
         * 根据选择器获取 DOM 元素
         * @param {string} selector - CSS 选择器
         * @returns {Element|null}
         */
        getElementBySelector(selector) {
            if (!selector) return null;
            if (selector.startsWith('#')) {
                return document.getElementById(selector.slice(1));
            }
            if (selector.startsWith('.')) {
                return document.querySelector(selector);
            }
            return document.getElementById(selector) || document.querySelector(selector);
        },

        /**
         * 验证并限制字符数范围
         * @param {number|null} value - 输入值
         * @returns {number|null}
         */
        clampCharsPerPage(value) {
            if (value === null) return null;
            if (value < CONSTANTS.CONFIG.CHARS_PER_PAGE_MIN) {
                return CONSTANTS.CONFIG.CHARS_PER_PAGE_MIN;
            }
            if (value > CONSTANTS.CONFIG.CHARS_PER_PAGE_MAX) {
                return CONSTANTS.CONFIG.CHARS_PER_PAGE_MAX;
            }
            return value;
        }
    };

    // ============================================================================
    // 样式模块 (Styles)
    // ============================================================================

    const Styles = `
        @keyframes er-settings-in {
            to {
                opacity: 1;
                transform: translate(-50%, -50%) scale(1);
            }
        }
        @keyframes er-overlay-in {
            to { opacity: 1; }
        }
        @keyframes er-notification-in {
            to {
                opacity: 1;
                transform: translateX(0);
            }
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%) scale(0.95);
            background: white;
            padding: 16px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
            z-index: 10000;
            max-width: 380px;
            width: 90%;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            border-radius: 10px;
            opacity: 0;
            animation: er-settings-in 0.2s ease forwards;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} h2 {
            margin: 0 0 12px 0;
            color: #1a1a1a;
            font-size: 16px;
            font-weight: 600;
            letter-spacing: -0.3px;
            padding-bottom: 8px;
            border-bottom: 1px solid #eee;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} .er-form-group {
            margin-bottom: 10px;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} label {
            display: block;
            margin-bottom: 4px;
            font-weight: 500;
            color: #555;
            font-size: 12px;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} input[type="text"],
        #${CONSTANTS.IDs.SETTINGS_PANEL} input[type="number"] {
            width: 100%;
            padding: 7px 9px;
            border: 1px solid #ddd;
            border-radius: 5px;
            box-sizing: border-box;
            font-size: 13px;
            font-family: inherit;
            background: #fafafa;
            transition: all 0.15s ease;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} input[type="text"]:focus,
        #${CONSTANTS.IDs.SETTINGS_PANEL} input[type="number"]:focus {
            outline: none;
            border-color: #3b82f6;
            background: white;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} input[type="file"] {
            width: 100%;
            padding: 5px;
            border: 1px dashed #ddd;
            border-radius: 5px;
            box-sizing: border-box;
            background: #fafafa;
            cursor: pointer;
            font-size: 12px;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} #er-reading-progress {
            padding: 7px 9px;
            background: #f0f9ff;
            border-radius: 5px;
            color: #0369a1;
            font-size: 12px;
            text-align: center;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} small {
            display: block;
            margin-top: 2px;
            color: #999;
            font-size: 11px;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} .er-button-group {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            padding-top: 2px;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} button {
            background: #3b82f6;
            color: white;
            border: none;
            padding: 6px 11px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 500;
            font-family: inherit;
            transition: all 0.15s ease;
            flex: 0 0 auto;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} button:hover {
            background: #2563eb;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} .er-close-btn {
            background: #ef4444;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} .er-close-btn:hover {
            background: #dc2626;
        }

        #${CONSTANTS.IDs.SETTINGS_PANEL} .er-shortcut-hint {
            margin-top: 10px;
            padding: 6px;
            background: #f8f9fa;
            border-radius: 5px;
            font-size: 10px;
            color: #999;
            text-align: center;
        }

        #${CONSTANTS.IDs.OVERLAY} {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.3);
            z-index: 9999;
            opacity: 0;
            animation: er-overlay-in 0.2s ease forwards;
        }

        .${CONSTANTS.IDs.READER} {
            width: 100%;
            height: 600px;
            border: 1px solid #ddd;
            border-radius: 4px;
            overflow: hidden;
            background: white;
            min-height: 600px;
        }

        .${CONSTANTS.IDs.READER} iframe {
            width: 100%;
            height: 100%;
            border: none;
        }

        .${CONSTANTS.IDs.TEXT_READER} {
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
        }

        #${CONSTANTS.IDs.TEXT_DISPLAY} {
            flex: 1;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.8;
            font-family: Arial, sans-serif;
            font-size: 16px;
            white-space: pre-line;
            overflow-y: auto;
            height: calc(100vh - 120px);
        }
    `;

    /**
     * 注入样式到页面
     */
    function injectStyles() {
        if (!document.getElementById(CONSTANTS.IDs.STYLES)) {
            const style = document.createElement('style');
            style.id = CONSTANTS.IDs.STYLES;
            style.textContent = Styles;
            document.head.appendChild(style);
        }
    }

    // ============================================================================
    // 分页模块 (Pagination)
    // ============================================================================

    const Pagination = {
        /**
         * 按章节分页(每章一页)
         * @param {string} text - 完整文本
         * @returns {string[]} 页面数组
         */
        byChapter(text) {
            const pages = [];
            const sections = text.split(/\n\n## /);

            sections.forEach((section, index) => {
                let trimmed = section.trim();
                if (index > 0 && !trimmed.startsWith('## ')) {
                    trimmed = '## ' + trimmed;
                }
                if (trimmed) {
                    pages.push(trimmed);
                }
            });

            console.log(`[${CONSTANTS.NS}] 按章节分页完成: ${pages.length} 页`);
            return pages;
        },

        /**
         * 按字符数分页
         * @param {string} text - 完整文本
         * @param {number} charsPerPage - 每页字符数
         * @returns {string[]} 页面数组
         */
        byChars(text, charsPerPage) {
            const pages = [];
            let currentPage = '';
            let currentLength = 0;
            const sections = text.split(/\n\n## /);

            sections.forEach((section, index) => {
                let s = section;
                if (index > 0 && !s.startsWith('## ')) {
                    s = '## ' + s;
                }

                // 超长章节需进一步分割
                if (s.length > charsPerPage) {
                    if (currentPage.trim()) {
                        pages.push(currentPage.trim());
                        currentPage = '';
                        currentLength = 0;
                    }

                    const paragraphs = s.split(/\n{2,}/);
                    paragraphs.forEach(para => {
                        const trimmedPara = para.trim();
                        if (!trimmedPara) return;

                        const separator = currentPage.length > 0 ? '\n\n' : '';
                        if (currentLength + trimmedPara.length + separator.length > charsPerPage) {
                            if (currentPage.trim()) {
                                pages.push(currentPage.trim());
                            }
                            currentPage = trimmedPara;
                            currentLength = trimmedPara.length;
                        } else {
                            currentPage += separator + trimmedPara;
                            currentLength += trimmedPara.length + separator.length;
                        }
                    });
                } else {
                    const separator = currentPage.length > 0 ? '\n\n' : '';
                    if (currentLength + s.length + separator.length > charsPerPage) {
                        if (currentPage.trim()) {
                            pages.push(currentPage.trim());
                        }
                        currentPage = s;
                        currentLength = s.length;
                    } else {
                        currentPage += separator + s;
                        currentLength += s.length + separator.length;
                    }
                }
            });

            if (currentPage.trim()) {
                pages.push(currentPage.trim());
            }

            if (pages.length === 0 && text.trim()) {
                pages.push(text.trim());
            }

            console.log(`[${CONSTANTS.NS}] 按字符数分页完成: ${pages.length} 页, 每页 ${charsPerPage} 字`);
            return pages;
        }
    };

    // ============================================================================
    // UI 模块 (Settings Panel)
    // ============================================================================

    const Panel = {
        /**
         * 创建设置面板
         */
        create: function() {
            if (State.settingsPanel) {
                State.settingsPanel.remove();
            }

            const overlay = document.createElement('div');
            overlay.id = CONSTANTS.IDs.OVERLAY;

            const panel = document.createElement('div');
            panel.id = CONSTANTS.IDs.SETTINGS_PANEL;
            panel.innerHTML = `
                <h2>EPUB 阅读器设置</h2>

                <div class="er-form-group">
                    <label for="er-file">选择EPUB文件:</label>
                    <input type="file" id="er-file" accept=".epub">
                </div>

                <div class="er-form-group">
                    <label for="er-div-selector">目标DIV选择器:</label>
                    <input type="text" id="er-div-selector" placeholder="如: #content 或 .main-content">
                </div>

                <div class="er-form-group">
                    <label for="er-page-number">跳转到页码:</label>
                    <input type="number" id="er-page-number" min="1" placeholder="输入页码">
                </div>

                <div class="er-form-group" id="er-chars-group" style="display: none;">
                    <label for="er-chars-per-page">每页字符数 (留空按章节):</label>
                    <input type="number" id="er-chars-per-page" min="${CONSTANTS.CONFIG.CHARS_PER_PAGE_MIN}" max="${CONSTANTS.CONFIG.CHARS_PER_PAGE_MAX}" placeholder="留空按章节">
                    <small>范围: ${CONSTANTS.CONFIG.CHARS_PER_PAGE_MIN} - ${CONSTANTS.CONFIG.CHARS_PER_PAGE_MAX} 字</small>
                </div>

                <div class="er-form-group">
                    <label>阅读进度:</label>
                    <div id="${CONSTANTS.IDs.READING_PROGRESS}">未开始阅读</div>
                </div>

                <div class="er-button-group">
                    <button id="er-apply">应用</button>
                    <button id="er-start-epub">EPUB模式</button>
                    <button id="er-start-text">文本模式</button>
                    <button class="er-close-btn" id="er-close">关闭</button>
                </div>

                <div class="er-shortcut-hint">
                    翻页: ← → 或 Cmd/Ctrl + [ ] · 设置: Cmd/Ctrl + B
                </div>
            `;

            document.body.appendChild(overlay);
            document.body.appendChild(panel);
            State.settingsPanel = panel;

            Panel._bindEvents();
            Panel._loadSettings();
        },

        /**
         * 绑定面板事件
         */
        _bindEvents: function() {
            const overlay = document.getElementById(CONSTANTS.IDs.OVERLAY);
            overlay.addEventListener('click', Panel.close);

            document.getElementById('er-close').addEventListener('click', Panel.close);
            document.getElementById('er-apply').addEventListener('click', Panel.applySettings);
            document.getElementById('er-start-epub').addEventListener('click', EpubReader.start);
            document.getElementById('er-start-text').addEventListener('click', TextReader.start);
            document.getElementById('er-file').addEventListener('change', Panel.handleFileSelect);
        },

        /**
         * 加载已保存的设置
         */
        _loadSettings: function() {
            const savedDiv = Utils.Storage.get(CONSTANTS.STORAGE.TARGET_DIV);
            if (savedDiv) {
                document.getElementById('er-div-selector').value = savedDiv;
            }

            const progress = Utils.Storage.get(CONSTANTS.STORAGE.BOOK_PROGRESS);
            const state = window[CONSTANTS.NS + 'State'];

            // 文本模式: 通过 keydownCleanup 判断(最可靠)
            const isTextMode = !!State.keydownCleanup;
            const charsGroup = document.getElementById('er-chars-group');
            const progressEl = document.getElementById(CONSTANTS.IDs.READING_PROGRESS);

            // 文本模式: 显示字符数设置组,从全局状态读取进度
            if (isTextMode && charsGroup) {
                charsGroup.style.display = 'block';

                if (state && state.totalPages > 0) {
                    const percentage = state.totalPages > 1
                        ? Math.round((state.currentPage / (state.totalPages - 1)) * 100)
                        : 100;
                    progressEl.textContent = `第 ${state.currentPage + 1} / ${state.totalPages} (${percentage}%)`;

                    const charsInput = document.getElementById('er-chars-per-page');
                    if (charsInput) {
                        charsInput.value = state.charsPerPage !== undefined && state.charsPerPage !== null ? state.charsPerPage : '';
                    }
                }
            } else if (charsGroup) {
                // EPUB 模式或其他: 隐藏字符数设置组
                charsGroup.style.display = 'none';

                // 从存储读取进度
                if (progress && progressEl) {
                    progressEl.textContent = `第 ${progress.currentPage} / ${progress.totalPages || '未知'}`;
                }
            }
        },

        /**
         * 关闭设置面板
         */
        close: function() {
            const overlay = document.getElementById(CONSTANTS.IDs.OVERLAY);
            const panel = document.getElementById(CONSTANTS.IDs.SETTINGS_PANEL);
            if (overlay) overlay.remove();
            if (panel) panel.remove();
            State.settingsPanel = null;
        },

        /**
         * 处理文件选择
         */
        handleFileSelect: async function(event) {
            const file = event.target.files[0];
            if (!file) return;

            if (!file.name.toLowerCase().endsWith('.epub')) {
                Utils.showNotification('请选择EPUB格式的文件', 'error');
                return;
            }

            try {
                const arrayBuffer = await Utils.readFileAsArrayBuffer(file);
                if (arrayBuffer.byteLength === 0) {
                    throw new Error('文件为空或损坏');
                }
                State.currentBookFile = arrayBuffer;
                Utils.showNotification('电子书已加载');
            } catch (error) {
                Utils.showNotification('加载失败: ' + error.message, 'error');
            }
        },

        /**
         * 应用设置(处理跳转和分页)
         */
        applySettings: function() {
            const divSelector = document.getElementById('er-div-selector').value.trim();
            if (!divSelector) {
                Utils.showNotification('请输入目标DIV选择器', 'error');
                return;
            }

            Utils.Storage.set(CONSTANTS.STORAGE.TARGET_DIV, divSelector);

            // 处理每页字符数(文本模式)
            const charsInput = document.getElementById('er-chars-per-page');
            if (charsInput) {
                const inputValue = charsInput.value.trim();
                let newCharsPerPage = inputValue ? parseInt(inputValue) : null;
                newCharsPerPage = Utils.clampCharsPerPage(newCharsPerPage);
                charsInput.value = newCharsPerPage || '';

                const readerInterface = window[CONSTANTS.NS + 'Interface'];
                if (readerInterface && readerInterface.repaginate) {
                    readerInterface.repaginate(newCharsPerPage);
                }
            }

            // 处理页码跳转
            const pageNumber = parseInt(document.getElementById('er-page-number').value);
            if (pageNumber && pageNumber > 0) {
                Panel._jumpToPage(pageNumber);
            } else {
                Utils.showNotification('设置已保存');
            }
        },

        /**
         * 跳转到指定页码
         */
        _jumpToPage: function(pageNumber) {
            // EPUB 模式
            if (State.rendition && State.book) {
                State.book.locations.generate(CONSTANTS.CONFIG.EPUB_LOCATION_INTERVAL)
                    .then(() => {
                        const location = State.book.locations.cfiFromLocation(pageNumber);
                        if (location) {
                            return State.rendition.goto(location);
                        }
                        throw new Error('页码超出范围');
                    })
                    .then(() => Utils.showNotification(`已跳转到第 ${pageNumber} 页`))
                    .catch(() => {
                        Utils.showNotification('跳转失败', 'error');
                    });
                return;
            }

            // 文本模式
            const readerInterface = window[CONSTANTS.NS + 'Interface'];
            if (readerInterface && readerInterface.displayPage) {
                const state = window[CONSTANTS.NS + 'State'];
                const totalPages = state ? state.totalPages : 0;
                if (pageNumber <= totalPages) {
                    readerInterface.displayPage(pageNumber - 1);
                    Utils.showNotification(`已跳转到第 ${pageNumber} 页`);
                } else {
                    Utils.showNotification(`页码超出范围,共 ${totalPages} 页`, 'error');
                }
                return;
            }

            Utils.showNotification('请先启动阅读器', 'error');
        }
    };

    // ============================================================================
    // EPUB 阅读器模块
    // ============================================================================

    const EpubReader = {
        /**
         * 启动 EPUB 阅读器
         */
        start: async function() {
            // 先停止当前阅读器
            stopReader();

            const divSelector = document.getElementById('er-div-selector').value.trim() ||
                Utils.Storage.get(CONSTANTS.STORAGE.TARGET_DIV);
            const pageNumber = parseInt(document.getElementById('er-page-number').value) || 1;

            if (!divSelector) {
                Utils.showNotification('请先设置目标DIV选择器', 'error');
                return;
            }

            const file = await EpubReader._getBookFile();
            if (!file) {
                Utils.showNotification('请先选择EPUB文件', 'error');
                return;
            }

            State.currentBookFile = file;

            try {
                await EpubReader._initialize(divSelector, pageNumber);
                Panel.close();
            } catch (error) {
                Utils.showNotification('启动失败: ' + error.message, 'error');
            }
        },

        /**
         * 获取 EPUB 文件
         */
        _getBookFile: function() {
            if (State.currentBookFile) return State.currentBookFile;
            const fileInput = document.getElementById('er-file');
            if (fileInput && fileInput.files && fileInput.files[0]) {
                return Utils.readFileAsArrayBuffer(fileInput.files[0]);
            }
            return null;
        },

        /**
         * 初始化 EPUB 阅读器
         */
        _initialize: async function(divSelector, startPage = 1) {
            const targetDiv = Utils.getElementBySelector(divSelector);
            if (!targetDiv) {
                throw new Error('找不到指定的DIV元素: ' + divSelector);
            }

            if (!State.currentBookFile) {
                throw new Error('请先选择EPUB文件');
            }

            // 清空目标DIV并创建容器
            targetDiv.innerHTML = '';
            const readerContainer = document.createElement('div');
            readerContainer.id = CONSTANTS.IDs.READER;
            readerContainer.className = CONSTANTS.IDs.READER;
            targetDiv.appendChild(readerContainer);

            readerContainer.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">正在加载...</div>';

            // 暴露 JSZip 到全局
            if (typeof unsafeWindow !== 'undefined') {
                unsafeWindow.JSZip = JSZip;
                unsafeWindow.ePub = ePub;
            } else {
                window.JSZip = JSZip;
                window.ePub = ePub;
            }

            try {
                State.book = ePub(State.currentBookFile);
                State.rendition = State.book.renderTo(CONSTANTS.IDs.READER, {
                    width: '100%',
                    height: '600px',
                    spread: 'always',
                    allowScriptedContent: true
                });

                // 加载书籍
                State.book.ready
                    .then(() => State.book.loaded.metadata)
                    .then(() => {
                        readerContainer.innerHTML = '';
                        return State.rendition.display();
                    })
                    .then(() => EpubReader._jumpToPage(startPage));

                // 设置事件监听
                State.rendition.on('relocated', (location) => {
                    const percentage = location.start.percentage || 0;
                    const currentLocation = location.start.location || 0;
                    const currentPage = Math.max(1, location.start.location || 0);
                    const totalPages = State.book.locations && State.book.locations.length();
                    Utils.Storage.set(CONSTANTS.STORAGE.BOOK_PROGRESS, {
                        currentPage,
                        totalPages: totalPages || '未知',
                        percentage
                    });
                    console.log(`[${CONSTANTS.NS}] EPUB 翻页: 第${currentPage}页, ${totalPages || '未知'}总页`);
                });

                State.rendition.on('error', (error) => {
                    readerContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: red;">渲染错误: ${error.message}</div>`;
                });

                State.rendition.on('rendered', (section) => {
                    console.log(`[${CONSTANTS.NS}] 章节渲染完成`);
                });

                // 生成位置信息
                if (startPage <= 1) {
                    State.book.locations.generate(CONSTANTS.CONFIG.EPUB_LOCATION_INTERVAL).catch(() => {});
                }

            } catch (error) {
                readerContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: red;">初始化失败: ${error.message}</div>`;
                throw error;
            }

            State.isReaderActive = true;
            Utils.Storage.set(CONSTANTS.STORAGE.TARGET_DIV, divSelector);
        },

        /**
         * 跳转到指定页码
         */
        _jumpToPage(pageNumber) {
            if (pageNumber <= 1) return;
            State.book.locations.generate(CONSTANTS.CONFIG.EPUB_LOCATION_INTERVAL)
                .then(() => {
                    const location = State.book.locations.cfiFromLocation(pageNumber);
                    if (location) {
                        return State.rendition.goto(location);
                    }
                })
                .catch(() => {});
        }
    };

    // ============================================================================
    // 纯文本阅读器模块
    // ============================================================================

    const TextReader = {
        /**
         * 启动纯文本模式
         */
        start: async function() {
            // 先停止当前阅读器
            stopReader();

            const divSelector = document.getElementById('er-div-selector').value.trim() ||
                Utils.Storage.get(CONSTANTS.STORAGE.TARGET_DIV);

            if (!divSelector) {
                Utils.showNotification('请先设置目标DIV选择器', 'error');
                return;
            }

            let file = null;
            if (State.currentBookFile) {
                file = State.currentBookFile;
            } else {
                const fileInput = document.getElementById('er-file');
                if (fileInput && fileInput.files && fileInput.files[0]) {
                    file = await Utils.readFileAsArrayBuffer(fileInput.files[0]);
                }
            }

            if (!file) {
                Utils.showNotification('请先选择EPUB文件', 'error');
                return;
            }

            State.currentBookFile = file;

            const targetDiv = Utils.getElementBySelector(divSelector);
            if (!targetDiv) {
                Utils.showNotification('找不到指定的DIV元素', 'error');
                return;
            }

            try {
                const initData = await TextReader._initialize(file, targetDiv);
                const readerInterface = TextReader._createInterface(targetDiv, initData.pages, initData.metadata, initData.allText, null);

                console.log(`[${CONSTANTS.NS}] 纯文本模式启动: ${initData.chapters.length} 章, ${initData.pages.length} 页`);
                Panel.close();
                Utils.showNotification('纯文本模式已启动');
            } catch (error) {
                Utils.showNotification('启动失败: ' + error.message, 'error');
            }
        },

        /**
         * 初始化纯文本模式
         */
        _initialize: async function(arrayBuffer, targetDiv) {
            State.isReaderActive = true;

            targetDiv.innerHTML = '';
            const container = document.createElement('div');
            container.id = CONSTANTS.IDs.TEXT_READER;
            container.className = CONSTANTS.IDs.TEXT_READER;
            targetDiv.appendChild(container);
            container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">正在加载...</div>';

            State.book = ePub(arrayBuffer);
            await State.book.ready;

            const metadata = await State.book.loaded.metadata;
            const navigation = await State.book.loaded.navigation;

            let allText = `${metadata.title || '未知标题'}\n作者: ${metadata.creator || '未知作者'}\n\n`;
            const chapters = [];

            for (const item of State.book.spine.items) {
                try {
                    const section = await State.book.load(item.href);
                    let content = section.documentElement?.textContent ||
                                   section.body?.textContent ||
                                   section.textContent || '';

                    content = content.trim();
                    if (!content) continue;

                    // 获取章节标题
                    let chapterTitle = TextReader._extractChapterTitle(navigation, item, content, chapters.length);

                    chapters.push({ title: chapterTitle, content });
                    allText += `\n\n## ${chapterTitle}\n\n${content}`;
                } catch (error) {
                    console.warn(`[${CONSTANTS.NS}] 加载章节失败:`, error);
                }
            }

            container.innerHTML = '';

            // 默认按章节分页
            const pages = Pagination.byChapter(allText);

            return { chapters, metadata, allText, pages };
        },

        /**
         * 提取章节标题
         */
        _extractChapterTitle: function(navigation, item, content, index) {
            // 尝试从导航获取
            if (Array.isArray(navigation)) {
                const navItem = navigation.find(nav => {
                    if (!nav || !nav.href || !item.href) return false;
                    return nav.href === item.href ||
                           nav.href.endsWith(item.href) ||
                           item.href.endsWith(nav.href);
                });
                if (navItem && navItem.label) return navItem.label;
            }
            if (item.title) return item.title;

            // 从内容提取
            const titleMatch = content.match(/<(h[1-6]|title)[^>]*>(.*?)<\/\1>/i);
            if (titleMatch) return titleMatch[2].trim();

            // 从文件名提取
            const fileName = item.href.split('/').pop().replace(/\.[^/.]+$/, '');
            return fileName || `第${index + 1}章`;
        },

        /**
         * 创建阅读器界面
         */
        _createInterface: function(targetDiv, pages, metadata, allText, charsPerPage) {
            targetDiv.innerHTML = '';
            targetDiv.style.cssText = `
                position: relative;
                height: 100%;
                display: flex;
                flex-direction: column;
            `;

            const textDisplay = document.createElement('div');
            textDisplay.id = CONSTANTS.IDs.TEXT_DISPLAY;
            textDisplay.style.cssText = `
                flex: 1;
                max-width: 800px;
                margin: 0 auto;
                padding: 20px;
                line-height: 1.8;
                font-family: Arial, sans-serif;
                font-size: 16px;
                white-space: pre-line;
                overflow-y: auto;
                height: calc(100vh - 120px);
            `;

            const content = document.createElement('div');
            content.id = CONSTANTS.IDs.PAGE_CONTENT;

            textDisplay.appendChild(TextReader._createHeader(metadata));
            textDisplay.appendChild(document.createElement('hr'));
            textDisplay.appendChild(content);
            targetDiv.appendChild(textDisplay);

            // 状态管理
            let currentPage = 0;
            let currentPages = pages;

            // 保存到全局
            const updateState = () => {
                window[CONSTANTS.NS + 'State'] = {
                    currentPage,
                    totalPages: currentPages.length,
                    charsPerPage
                };
                TextReader._updateProgressDisplay(currentPage, currentPages.length);
            };

            // 显示页面
            const displayPage = (pageIndex) => {
                if (pageIndex < 0) pageIndex = 0;
                if (pageIndex >= currentPages.length) pageIndex = currentPages.length - 1;

                currentPage = pageIndex;
                content.textContent = currentPages[pageIndex];
                textDisplay.scrollTop = 0;
                updateState();
            };

            // 重新分页
            const repaginate = (newCharsPerPage) => {
                const ratio = currentPages.length > 1 ? currentPage / (currentPages.length - 1) : 0;
                currentPages = newCharsPerPage === null
                    ? Pagination.byChapter(allText)
                    : Pagination.byChars(allText, newCharsPerPage);
                charsPerPage = newCharsPerPage;

                const newPage = Math.min(
                    Math.floor(ratio * Math.max(currentPages.length - 1, 1)),
                    currentPages.length - 1
                );
                displayPage(newPage);
            };

            // 键盘控制
            const handleKeydown = (event) => {
                // 文本模式键盘控制
                if (!State.isReaderActive) return;

                const isCmdOrCtrl = event.metaKey || event.ctrlKey;

                switch (event.key) {
                    case 'ArrowLeft':
                        event.preventDefault();
                        displayPage(currentPage - 1);
                        break;
                    case 'ArrowRight':
                        event.preventDefault();
                        displayPage(currentPage + 1);
                        break;
                    case '[':
                        if (isCmdOrCtrl) {
                            event.preventDefault();
                            displayPage(currentPage - 1);
                        }
                        break;
                    case ']':
                        if (isCmdOrCtrl) {
                            event.preventDefault();
                            displayPage(currentPage + 1);
                        }
                        break;
                }
            };

            // 先清理旧的键盘事件监听器
            if (State.keydownCleanup) {
                State.keydownCleanup();
            }

            document.addEventListener('keydown', handleKeydown);
            State.keydownCleanup = () => document.removeEventListener('keydown', handleKeydown);

            // 保存接口到全局
            const readerInterface = { displayPage, repaginate };
            window[CONSTANTS.NS + 'Interface'] = readerInterface;

            displayPage(0);
            return readerInterface;
        },

        /**
         * 创建书籍头部信息
         */
        _createHeader: function(metadata) {
            const wrapper = document.createElement('div');

            const title = document.createElement('h1');
            title.textContent = metadata.title || '未知标题';
            title.style.cssText = 'margin-bottom: 10px;';

            const author = document.createElement('p');
            author.innerHTML = `<strong>作者:</strong> ${metadata.creator || '未知作者'}`;
            author.style.cssText = 'margin-bottom: 20px; color: #666;';

            wrapper.appendChild(title);
            wrapper.appendChild(author);
            return wrapper;
        },

        /**
         * 更新进度显示
         */
        _updateProgressDisplay: function(currentPage, totalPages) {
            const progressEl = document.getElementById(CONSTANTS.IDs.READING_PROGRESS);
            if (progressEl && totalPages > 0) {
                const percentage = totalPages > 1
                    ? Math.round((currentPage / (totalPages - 1)) * 100)
                    : 100;
                progressEl.textContent = `第 ${currentPage + 1} / ${totalPages} (${percentage}%)`;
            }
        }
    };

    // ============================================================================
    // 键盘控制模块
    // ============================================================================

    const Keyboard = {
        /**
         * 添加键盘事件监听
         */
        bind: function() {
            document.addEventListener('keydown', Keyboard.handle);
        },

        /**
         * 移除键盘事件监听
         */
        unbind: function() {
            document.removeEventListener('keydown', Keyboard.handle);
        },

        /**
         * 处理键盘事件
         */
        handle: function(event) {
            // Cmd/Ctrl + B: 打开设置面板(始终可用)
            if ((event.metaKey || event.ctrlKey) && event.key === 'b') {
                event.preventDefault();
                Panel.create();
                return;
            }

            // 检查阅读器是否激活
            if (!State.isReaderActive) return;

            // 文本模式由内部处理
            if (State.keydownCleanup) return;

            // EPUB 模式翻页
            if (!State.rendition) return;

            const isCmdOrCtrl = event.metaKey || event.ctrlKey;

            switch (event.key) {
                case 'ArrowLeft':
                    event.preventDefault();
                    State.rendition.prev();
                    break;
                case 'ArrowRight':
                    event.preventDefault();
                    State.rendition.next();
                    break;
                case '[':
                    if (isCmdOrCtrl) {
                        event.preventDefault();
                        State.rendition.prev();
                    }
                    break;
                case ']':
                    if (isCmdOrCtrl) {
                        event.preventDefault();
                        State.rendition.next();
                    }
                    break;
            }
        }
    };

    // ============================================================================
    // 停止阅读器
    // ============================================================================

    function stopReader() {
        if (State.book) {
            State.book.destroy();
            State.book = null;
        }

        if (State.rendition) {
            State.rendition.destroy();
            State.rendition = null;
        }

        // 清理文本模式键盘事件
        if (State.keydownCleanup) {
            State.keydownCleanup();
            State.keydownCleanup = null;
        }

        // 移除容器
        const readerEl = document.getElementById(CONSTANTS.IDs.READER);
        if (readerEl) readerEl.remove();

        const textReaderEl = document.getElementById(CONSTANTS.IDs.TEXT_READER);
        if (textReaderEl) textReaderEl.remove();

        State.isReaderActive = false;

        // 清理全局状态
        window[CONSTANTS.NS + 'Interface'] = null;
        window[CONSTANTS.NS + 'State'] = null;
    }

    // ============================================================================
    // 初始化
    // ============================================================================

    /**
     * 确保依赖库在全局可用
     */
    function ensureLibraries() {
        if (typeof JSZip === 'undefined') {
            console.error(`[${CONSTANTS.NS}] JSZip 库未加载`);
            Utils.showNotification('JSZip库加载失败,请刷新页面', 'error');
            return false;
        }

        if (typeof unsafeWindow !== 'undefined') {
            unsafeWindow.JSZip = JSZip;
            unsafeWindow.ePub = ePub;
        } else {
            window.JSZip = JSZip;
            window.ePub = ePub;
        }

        return true;
    }

    /**
     * 主初始化函数
     */
    function init() {
        // 检查依赖
        if (!ensureLibraries()) return;

        // 注入样式
        injectStyles();

        // 绑定全局键盘事件
        Keyboard.bind();

        // 加载保存的设置
        const savedDiv = Utils.Storage.get(CONSTANTS.STORAGE.TARGET_DIV);
        if (savedDiv) {
            console.log(`[${CONSTANTS.NS}] 已加载上次使用的DIV选择器: ${savedDiv}`);
        }
    }

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

})(); // IIFE 结束