Greasy Fork

Greasy Fork is available in English.

小说阅读器 (极简稳定版)

回归初心,最简单的样式。右下角固定一个小按钮开关,也可以用 Alt+M。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         小说阅读器 (极简稳定版)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  回归初心,最简单的样式。右下角固定一个小按钮开关,也可以用 Alt+M。
// @author       Gemini
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      *
// ==/UserScript==

(function() {
    'use strict';
    
    // 防止在 iframe 中无限嵌套
    if (window.top !== window.self) return;

    // --- 1. 创建一个永远可见的实体开关按钮 ---
    const toggleBtn = document.createElement('button');
    toggleBtn.textContent = '📚';
    toggleBtn.title = '打开/关闭阅读器';
    toggleBtn.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 2147483647;
        padding: 5px 10px;
        cursor: pointer;
        border: 1px solid #ccc;
        background: white;
        border-radius: 4px;
        opacity: 0.6;
        font-size: 16px;
    `;
    document.body.appendChild(toggleBtn);

    // --- 2. 创建最简单的主容器 ---
    const container = document.createElement('div');
    container.style.cssText = `
        position: fixed;
        bottom: 60px;
        right: 20px;
        width: 350px;
        height: 500px;
        background: white;
        border: 1px solid #ccc;
        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        z-index: 2147483647;
        display: none; /* 默认隐藏 */
        flex-direction: column;
        resize: both;
        overflow: hidden;
        opacity: 0.85;
        font-family: sans-serif;
    `;
    document.body.appendChild(container);

    // --- 3. 顶部控制栏 (只保留输入框、按钮和透明度滑块) ---
    const header = document.createElement('div');
    header.style.cssText = 'padding: 6px; background: #f8f9fa; border-bottom: 1px solid #ddd; display: flex; gap: 5px; align-items: center;';
    
    const urlInput = document.createElement('input');
    urlInput.placeholder = '输入网址...';
    urlInput.style.cssText = 'flex-grow: 1; padding: 4px; font-size: 12px; border: 1px solid #ccc;';
    urlInput.value = GM_getValue('moyu_last_url', '');

    const loadBtn = document.createElement('button');
    loadBtn.textContent = '加载';
    loadBtn.style.cssText = 'padding: 4px 8px; font-size: 12px; cursor: pointer; background: white; border: 1px solid #ccc;';

    const opacityInput = document.createElement('input');
    opacityInput.type = 'range'; opacityInput.min = '0.1'; opacityInput.max = '1'; opacityInput.step = '0.05'; opacityInput.value = '0.85';
    opacityInput.style.cssText = 'width: 60px; cursor: pointer;';

    header.appendChild(urlInput);
    header.appendChild(loadBtn);
    header.appendChild(opacityInput);

    // --- 4. 内容区 ---
    const iframe = document.createElement('iframe');
    iframe.style.cssText = 'width: 100%; flex-grow: 1; border: none; background: #fff;';
    
    container.appendChild(header);
    container.appendChild(iframe);

    // --- 5. 核心加载逻辑 ---
    const loadUrl = () => {
        let url = urlInput.value.trim();
        if (!url) return;
        if (!url.startsWith('http')) url = 'https://' + url;
        loadBtn.textContent = '...';
        
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            headers: { "User-Agent": navigator.userAgent, "Referer": new URL(url).origin + "/" },
            onload: (res) => {
                loadBtn.textContent = '加载';
                if(res.status !== 200) { iframe.srcdoc = `<div style="padding:20px;color:red;">加载失败,状态码: ${res.status}</div>`; return; }
                
                let html = res.responseText;
                const baseTag = `<base href="${url}">`;
                const script = `<script>document.addEventListener('click', e => { let a = e.target.closest('a'); if(a && a.href && !a.href.startsWith('javascript:')) { e.preventDefault(); window.parent.postMessage({type:'NAV', url:a.href}, '*'); } });</script>`;
                
                if(html.includes('<head>')) {
                    html = html.replace('<head>', `<head>${baseTag}`);
                } else {
                    html = baseTag + html;
                }
                iframe.srcdoc = html + script;
                GM_setValue('moyu_last_url', url);
            },
            onerror: () => { loadBtn.textContent = '加载'; iframe.srcdoc = '<div style="padding:20px;color:red;">网络请求失败</div>'; }
        });
    };

    // --- 6. 事件绑定 ---
    loadBtn.onclick = loadUrl;
    urlInput.onkeypress = (e) => { if(e.key === 'Enter') loadUrl(); };
    
    // 透明度实时调节
    opacityInput.oninput = (e) => { container.style.opacity = e.target.value; };
    
    // 拦截内部跳转
    window.addEventListener('message', (e) => {
        if(e.data && e.data.type === 'NAV') { urlInput.value = e.data.url; loadUrl(); }
    });

    // --- 7. 开关逻辑 ---
    const toggle = () => {
        container.style.display = container.style.display === 'none' ? 'flex' : 'none';
        if(container.style.display === 'flex' && urlInput.value && !iframe.srcdoc) loadUrl();
    };

    // 按钮点击开关
    toggleBtn.onclick = toggle;

    // 快捷键开关 (Alt+M 作为辅助)
    document.addEventListener('keydown', (e) => {
        if (e.altKey && e.key.toLowerCase() === 'm') toggle();
    });

})();