Greasy Fork镜像 还支持 简体中文。

漫画人现代阅读器(双页+垂直模式)

现代化漫画阅读器:支持双页分栏和垂直滚动模式,触控优化,缩放功能

// ==UserScript==
// @name         漫画人现代阅读器(双页+垂直模式)
// @namespace    https://tampermonkey.net/
// @version      3.0.0
// @description  现代化漫画阅读器:支持双页分栏和垂直滚动模式,触控优化,缩放功能
// @author       you
// @match        https://www.manhuaren.com/m*/
// @match        https://www.manhuaren.com/m*/*
// @run-at       document-idle
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // ===== 工具函数 =====
  const $ = (sel, root = document) => root.querySelector(sel);
  const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
  const store = {
    get(k, def) { try { return JSON.parse(localStorage.getItem(k)) ?? def; } catch { return def; } },
    set(k, v) { localStorage.setItem(k, JSON.stringify(v)); }
  };
  const clamp = (n, min, max) => Math.max(min, Math.min(max, n));

  // ===== 状态管理 =====
  const state = {
    pages: [],
    currentIndex: 0,
    mode: store.get('mcr_mode', 'dual'),           // 'dual' | 'vertical'
    rtl: store.get('mcr_rtl', true),               // 右到左
    dualFit: store.get('mcr_dualFit', 'height'),   // 'height' | 'width'
    dualGap: store.get('mcr_dualGap', 16),
    firstSingle: store.get('mcr_firstSingle', true),
    verticalZoom: store.get('mcr_verticalZoom', 100), // 50-200%
    settingsOpen: false,
    
    save() {
      store.set('mcr_mode', this.mode);
      store.set('mcr_rtl', this.rtl);
      store.set('mcr_dualFit', this.dualFit);
      store.set('mcr_dualGap', this.dualGap);
      store.set('mcr_firstSingle', this.firstSingle);
      store.set('mcr_verticalZoom', this.verticalZoom);
    }
  };

  // ===== 图片收集 =====
  async function collectImages(maxWait = 15000) {
    const start = Date.now();
    while (Date.now() - start < maxWait) {
      if (window.newImgs?.length) return [...new Set(window.newImgs.map(s => s.replace(/^\/\//, location.protocol + '//')))];
      const domImgs = $$('#cp_img img').map(n => n.dataset.src || n.src).filter(Boolean);
      if (domImgs.length) {
        await new Promise(r => setTimeout(r, 200));
        if (window.newImgs?.length) return [...new Set(window.newImgs.map(s => s.replace(/^\/\//, location.protocol + '//')))];
        return [...new Set(domImgs.map(s => s.replace(/^\/\//, location.protocol + '//')))];
      }
      await new Promise(r => setTimeout(r, 150));
    }
    return [];
  }

  // ===== 样式 =====
  const CSS = `
    :root {
      --mcr-bg: #0a0a0a;
      --mcr-surface: rgba(20, 20, 20, 0.95);
      --mcr-border: rgba(255, 255, 255, 0.08);
      --mcr-text: #e8e8e8;
      --mcr-text-dim: #a0a0a0;
      --mcr-primary: #4a9eff;
      --mcr-primary-hover: #5dadff;
      --mcr-gap: 16px;
      --mcr-radius: 12px;
      --mcr-transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
    }

    #mcr-root {
      position: fixed;
      inset: 0;
      z-index: 2147483647;
      background: var(--mcr-bg);
      color: var(--mcr-text);
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
      overflow: hidden;
      touch-action: none;
    }

    /* ===== 顶栏 ===== */
    #mcr-header {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      height: 56px;
      background: var(--mcr-surface);
      backdrop-filter: blur(20px);
      border-bottom: 1px solid var(--mcr-border);
      display: flex;
      align-items: center;
      padding: 0 16px;
      gap: 12px;
      z-index: 100;
      transition: var(--mcr-transition);
    }

    #mcr-header.hidden { transform: translateY(-100%); }

    .mcr-btn {
      height: 38px;
      padding: 0 16px;
      border: none;
      border-radius: 8px;
      background: rgba(255, 255, 255, 0.08);
      color: var(--mcr-text);
      font-size: 13px;
      font-weight: 500;
      cursor: pointer;
      transition: var(--mcr-transition);
      white-space: nowrap;
    }

    .mcr-btn:hover { background: rgba(255, 255, 255, 0.15); }
    .mcr-btn:active { transform: scale(0.96); }
    .mcr-btn.primary { background: var(--mcr-primary); color: #fff; }
    .mcr-btn.primary:hover { background: var(--mcr-primary-hover); }
    .mcr-btn:disabled { opacity: 0.4; cursor: not-allowed; }

    .mcr-icon-btn {
      width: 38px;
      height: 38px;
      padding: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 18px;
    }

    .mcr-spacer { flex: 1; }

    .mcr-progress {
      font-size: 13px;
      color: var(--mcr-text-dim);
      padding: 0 8px;
    }

    /* ===== 双页模式 ===== */
    #mcr-dual-container {
      position: absolute;
      top: 56px;
      left: 0;
      right: 0;
      bottom: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: var(--mcr-gap);
      overflow: hidden;
    }

    #mcr-dual-container.fit-height .mcr-page-img {
      max-height: calc(100vh - 56px);
      width: auto;
      height: auto;
    }

    #mcr-dual-container.fit-width {
      align-items: flex-start;
      overflow: auto;
    }

    #mcr-dual-container.fit-width .mcr-page-img {
      width: calc((100vw - var(--mcr-gap) - 32px) / 2);
      height: auto;
    }

    .mcr-page-wrap {
      position: relative;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: opacity 0.3s;
    }

    .mcr-page-wrap.hidden { visibility: hidden; opacity: 0; }

    .mcr-page-img {
      display: block;
      max-width: 100%;
      border-radius: 8px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
      user-select: none;
      -webkit-user-drag: none;
    }

    .mcr-page-num {
      position: absolute;
      bottom: 12px;
      right: 12px;
      background: rgba(0, 0, 0, 0.7);
      backdrop-filter: blur(8px);
      padding: 6px 12px;
      border-radius: 16px;
      font-size: 12px;
      font-weight: 600;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .mcr-page-wrap.left .mcr-page-num { right: auto; left: 12px; }

    /* ===== 垂直模式 ===== */
    #mcr-vertical-container {
      position: absolute;
      top: 56px;
      left: 0;
      right: 0;
      bottom: 0;
      overflow-y: auto;
      overflow-x: hidden;
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 20px 0;
      gap: 12px;
    }

    #mcr-vertical-container .mcr-page-img {
      max-width: 95vw;
      height: auto;
      transition: transform 0.2s;
    }

    /* ===== 点击区域 ===== */
    #mcr-click-overlay {
      position: absolute;
      inset: 56px 0 0 0;
      display: grid;
      grid-template-columns: 1fr 1fr;
      pointer-events: all;
      z-index: 50;
    }

    .mcr-click-zone {
      cursor: pointer;
      transition: background 0.2s;
    }

    .mcr-click-zone:active { background: rgba(255, 255, 255, 0.03); }

    /* ===== 设置面板 ===== */
    #mcr-settings {
      position: fixed;
      top: 56px;
      right: -360px;
      width: 340px;
      bottom: 0;
      background: var(--mcr-surface);
      backdrop-filter: blur(20px);
      border-left: 1px solid var(--mcr-border);
      padding: 24px;
      overflow-y: auto;
      transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      z-index: 101;
    }

    #mcr-settings.open { right: 0; }

    #mcr-settings h3 {
      margin: 0 0 16px 0;
      font-size: 16px;
      font-weight: 600;
    }

    .mcr-setting-group {
      margin-bottom: 24px;
    }

    .mcr-setting-label {
      display: block;
      font-size: 13px;
      color: var(--mcr-text-dim);
      margin-bottom: 8px;
      font-weight: 500;
    }

    .mcr-btn-group {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }

    .mcr-btn-group .mcr-btn {
      flex: 1;
      min-width: 0;
    }

    .mcr-btn.active {
      background: var(--mcr-primary);
      color: #fff;
    }

    .mcr-slider-container {
      display: flex;
      align-items: center;
      gap: 12px;
    }

    .mcr-slider {
      flex: 1;
      height: 32px;
      -webkit-appearance: none;
      appearance: none;
      background: rgba(255, 255, 255, 0.08);
      border-radius: 16px;
      outline: none;
    }

    .mcr-slider::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      background: var(--mcr-primary);
      cursor: pointer;
      transition: var(--mcr-transition);
    }

    .mcr-slider::-webkit-slider-thumb:hover {
      transform: scale(1.2);
      background: var(--mcr-primary-hover);
    }

    .mcr-slider-value {
      min-width: 50px;
      text-align: right;
      font-size: 13px;
      font-weight: 600;
    }

    .mcr-checkbox {
      display: flex;
      align-items: center;
      gap: 8px;
      cursor: pointer;
      padding: 8px 0;
    }

    .mcr-checkbox input {
      width: 20px;
      height: 20px;
      cursor: pointer;
    }

    /* ===== 遮罩 ===== */
    #mcr-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.6);
      backdrop-filter: blur(2px);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.3s;
      z-index: 99;
    }

    #mcr-overlay.visible {
      opacity: 1;
      pointer-events: all;
    }

    /* ===== 隐藏原站 ===== */
    body.mcr-active > *:not(#mcr-root) { display: none !important; }
    html, body { overflow: hidden !important; }

    /* ===== 响应式 ===== */
    @media (max-width: 768px) {
      #mcr-header { padding: 0 8px; gap: 6px; height: 48px; }
      .mcr-btn { height: 36px; padding: 0 12px; font-size: 12px; }
      #mcr-settings { width: 100%; right: -100%; }
      .mcr-progress { display: none; }
    }
  `;

  // ===== HTML 结构 =====
  function createUI() {
    const root = document.createElement('div');
    root.id = 'mcr-root';
    root.innerHTML = `
      <div id="mcr-header">
        <button class="mcr-btn mcr-icon-btn" id="mcr-prev">←</button>
        <button class="mcr-btn mcr-icon-btn" id="mcr-next">→</button>
        <span class="mcr-progress" id="mcr-progress">1 / 1</span>
        <span class="mcr-spacer"></span>
        <button class="mcr-btn" id="mcr-mode-toggle">切换模式</button>
        <button class="mcr-btn" id="mcr-prev-ch">上一章</button>
        <button class="mcr-btn" id="mcr-next-ch">下一章</button>
        <button class="mcr-btn primary" id="mcr-settings-btn">⚙ 设置</button>
      </div>

      <div id="mcr-dual-container" class="fit-height" style="--mcr-gap: ${state.dualGap}px">
        <div class="mcr-page-wrap left">
          <img class="mcr-page-img" id="mcr-img-left" />
          <div class="mcr-page-num" id="mcr-num-left"></div>
        </div>
        <div class="mcr-page-wrap right">
          <img class="mcr-page-img" id="mcr-img-right" />
          <div class="mcr-page-num" id="mcr-num-right"></div>
        </div>
      </div>

      <div id="mcr-vertical-container" style="display: none;"></div>

      <div id="mcr-click-overlay">
        <div class="mcr-click-zone" id="mcr-zone-left"></div>
        <div class="mcr-click-zone" id="mcr-zone-right"></div>
      </div>

      <div id="mcr-overlay"></div>

      <div id="mcr-settings">
        <h3>阅读设置</h3>
        
        <div class="mcr-setting-group">
          <div class="mcr-setting-label">阅读模式</div>
          <div class="mcr-btn-group">
            <button class="mcr-btn" data-mode="dual">双页</button>
            <button class="mcr-btn" data-mode="vertical">垂直</button>
          </div>
        </div>

        <div class="mcr-setting-group" id="mcr-dual-settings">
          <div class="mcr-setting-label">双页方向</div>
          <div class="mcr-btn-group">
            <button class="mcr-btn" data-rtl="true">右→左</button>
            <button class="mcr-btn" data-rtl="false">左→右</button>
          </div>

          <div class="mcr-setting-label" style="margin-top: 16px;">双页适配</div>
          <div class="mcr-btn-group">
            <button class="mcr-btn" data-fit="height">按高度</button>
            <button class="mcr-btn" data-fit="width">按宽度</button>
          </div>

          <div class="mcr-setting-label" style="margin-top: 16px;">页面间距</div>
          <div class="mcr-slider-container">
            <input type="range" class="mcr-slider" id="mcr-gap-slider" min="0" max="48" step="4" value="${state.dualGap}">
            <span class="mcr-slider-value" id="mcr-gap-value">${state.dualGap}px</span>
          </div>

          <label class="mcr-checkbox">
            <input type="checkbox" id="mcr-first-single" ${state.firstSingle ? 'checked' : ''}>
            <span>首页单页显示</span>
          </label>
        </div>

        <div class="mcr-setting-group" id="mcr-vertical-settings" style="display: none;">
          <div class="mcr-setting-label">页面缩放</div>
          <div class="mcr-slider-container">
            <input type="range" class="mcr-slider" id="mcr-zoom-slider" min="50" max="200" step="5" value="${state.verticalZoom}">
            <span class="mcr-slider-value" id="mcr-zoom-value">${state.verticalZoom}%</span>
          </div>
        </div>
      </div>
    `;

    document.body.appendChild(root);
    document.body.classList.add('mcr-active');

    const style = document.createElement('style');
    style.textContent = CSS;
    document.head.appendChild(style);

    return root;
  }

  // ===== 事件绑定 =====
  function bindEvents() {
    // 导航按钮
    $('#mcr-prev').onclick = () => navigate(-1);
    $('#mcr-next').onclick = () => navigate(1);

    // 点击区域
    $('#mcr-zone-left').onclick = () => navigate(state.rtl && state.mode === 'dual' ? 1 : -1);
    $('#mcr-zone-right').onclick = () => navigate(state.rtl && state.mode === 'dual' ? -1 : 1);

    // 模式切换
    $('#mcr-mode-toggle').onclick = () => {
      state.mode = state.mode === 'dual' ? 'vertical' : 'dual';
      state.currentIndex = 0;
      state.save();
      updateUI();
      render();
    };

    // 设置按钮
    $('#mcr-settings-btn').onclick = () => {
      state.settingsOpen = !state.settingsOpen;
      $('#mcr-settings').classList.toggle('open', state.settingsOpen);
      $('#mcr-overlay').classList.toggle('visible', state.settingsOpen);
    };

    $('#mcr-overlay').onclick = () => {
      state.settingsOpen = false;
      $('#mcr-settings').classList.remove('open');
      $('#mcr-overlay').classList.remove('visible');
    };

    // 设置面板
    $$('[data-mode]').forEach(btn => {
      btn.onclick = () => {
        state.mode = btn.dataset.mode;
        state.currentIndex = 0;
        state.save();
        updateUI();
        render();
      };
    });

    $$('[data-rtl]').forEach(btn => {
      btn.onclick = () => {
        state.rtl = btn.dataset.rtl === 'true';
        state.save();
        updateUI();
        render();
      };
    });

    $$('[data-fit]').forEach(btn => {
      btn.onclick = () => {
        state.dualFit = btn.dataset.fit;
        state.save();
        updateUI();
        render();
      };
    });

    $('#mcr-gap-slider').oninput = (e) => {
      state.dualGap = parseInt(e.target.value);
      $('#mcr-gap-value').textContent = state.dualGap + 'px';
      $('#mcr-dual-container').style.setProperty('--mcr-gap', state.dualGap + 'px');
      state.save();
    };

    $('#mcr-zoom-slider').oninput = (e) => {
      state.verticalZoom = parseInt(e.target.value);
      $('#mcr-zoom-value').textContent = state.verticalZoom + '%';
      updateVerticalZoom();
      state.save();
    };

    $('#mcr-first-single').onchange = (e) => {
      state.firstSingle = e.target.checked;
      state.currentIndex = 0;
      state.save();
      render();
    };

    // 键盘
    window.addEventListener('keydown', (e) => {
      if (/INPUT|TEXTAREA|SELECT/.test(e.target.tagName)) return;
      
      switch (e.key) {
        case 'ArrowRight': case ' ': e.preventDefault(); navigate(1); break;
        case 'ArrowLeft': case 'Backspace': e.preventDefault(); navigate(-1); break;
        case 'm': case 'M': $('#mcr-mode-toggle').click(); break;
        case 's': case 'S': $('#mcr-settings-btn').click(); break;
        case 'r': case 'R': state.rtl = !state.rtl; state.save(); updateUI(); render(); break;
      }
    });

    // 触控滑动
    let touchStartX = 0;
    let touchStartY = 0;
    
    $('#mcr-root').addEventListener('touchstart', (e) => {
      if (e.target.closest('#mcr-settings')) return;
      touchStartX = e.touches[0].clientX;
      touchStartY = e.touches[0].clientY;
    }, { passive: true });

    $('#mcr-root').addEventListener('touchend', (e) => {
      if (e.target.closest('#mcr-settings')) return;
      const dx = e.changedTouches[0].clientX - touchStartX;
      const dy = e.changedTouches[0].clientY - touchStartY;
      
      if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
        navigate(dx > 0 ? -1 : 1);
      }
    }, { passive: true });

    // 章节导航
    const [prevUrl, nextUrl] = findChapterUrls();
    $('#mcr-prev-ch').disabled = !prevUrl;
    $('#mcr-next-ch').disabled = !nextUrl;
    if (prevUrl) $('#mcr-prev-ch').onclick = () => location.href = prevUrl;
    if (nextUrl) $('#mcr-next-ch').onclick = () => location.href = nextUrl;
  }

  function findChapterUrls() {
    const links = $$('a');
    const prev = links.find(a => a.textContent.trim() === '上一章');
    const next = links.find(a => a.textContent.trim() === '下一章');
    
    const getUrl = (a) => {
      if (!a) return '';
      const href = a.getAttribute('href') || '';
      const m = href.match(/pushHistory\('([^']+)'\)/);
      return m ? location.origin + m[1] : (href.startsWith('/') ? location.origin + href : '');
    };
    
    return [getUrl(prev), getUrl(next)];
  }

  // ===== 导航 =====
  function navigate(delta) {
    if (state.mode === 'dual') {
      const total = getDualPairCount();
      state.currentIndex = clamp(state.currentIndex + delta, 0, total - 1);
    } else {
      state.currentIndex = clamp(state.currentIndex + delta, 0, state.pages.length - 1);
    }
    render();
  }

  // ===== 双页逻辑 =====
  function getDualPairCount() {
    const n = state.pages.length;
    return state.firstSingle ? 1 + Math.ceil((n - 1) / 2) : Math.ceil(n / 2);
  }

  function getDualPair(idx) {
    const n = state.pages.length;
    
    if (state.firstSingle) {
      if (idx === 0) {
        return { left: null, right: state.pages[0], leftNum: null, rightNum: 1 };
      }
      const start = 1 + (idx - 1) * 2;
      let l = state.pages[start] || null;
      let r = state.pages[start + 1] || null;
      let ln = l ? start + 1 : null;
      let rn = r ? start + 2 : null;
      if (state.rtl) { [l, r, ln, rn] = [r, l, rn, ln]; }
      return { left: l, right: r, leftNum: ln, rightNum: rn };
    } else {
      const start = idx * 2;
      let l = state.pages[start] || null;
      let r = state.pages[start + 1] || null;
      let ln = l ? start + 1 : null;
      let rn = r ? start + 2 : null;
      if (state.rtl) { [l, r, ln, rn] = [r, l, rn, ln]; }
      return { left: l, right: r, leftNum: ln, rightNum: rn };
    }
  }

  // ===== 渲染 =====
  function render() {
    if (state.mode === 'dual') {
      renderDual();
    } else {
      renderVertical();
    }
    updateProgress();
  }

  function renderDual() {
    $('#mcr-dual-container').style.display = 'flex';
    $('#mcr-vertical-container').style.display = 'none';
    $('#mcr-click-overlay').style.display = 'grid';

    const { left, right, leftNum, rightNum } = getDualPair(state.currentIndex);

    const imgL = $('#mcr-img-left');
    const imgR = $('#mcr-img-right');
    const wrapL = imgL.closest('.mcr-page-wrap');
    const wrapR = imgR.closest('.mcr-page-wrap');

    if (left) {
      imgL.src = left;
      wrapL.classList.remove('hidden');
      $('#mcr-num-left').textContent = leftNum;
    } else {
      wrapL.classList.add('hidden');
    }

    if (right) {
      imgR.src = right;
      wrapR.classList.remove('hidden');
      $('#mcr-num-right').textContent = rightNum;
    } else {
      wrapR.classList.add('hidden');
    }

    // 预加载
    const nextIdx = state.currentIndex + 1;
    if (nextIdx < getDualPairCount()) {
      const next = getDualPair(nextIdx);
      [next.left, next.right].filter(Boolean).forEach(src => {
        const img = new Image();
        img.src = src;
      });
    }
  }

  function renderVertical() {
    $('#mcr-dual-container').style.display = 'none';
    $('#mcr-vertical-container').style.display = 'flex';
    $('#mcr-click-overlay').style.display = 'none';

    const container = $('#mcr-vertical-container');
    if (container.children.length === 0) {
      state.pages.forEach((src, i) => {
        const wrap = document.createElement('div');
        wrap.className = 'mcr-page-wrap';
        const img = document.createElement('img');
        img.className = 'mcr-page-img';
        img.src = src;
        img.alt = `第 ${i + 1} 页`;
        wrap.appendChild(img);
        container.appendChild(wrap);
      });
      updateVerticalZoom();
    }
  }

  function updateVerticalZoom() {
    const imgs = $$('#mcr-vertical-container .mcr-page-img');
    imgs.forEach(img => {
      img.style.transform = `scale(${state.verticalZoom / 100})`;
    });
  }

  function updateProgress() {
    const total = state.mode === 'dual' ? getDualPairCount() : state.pages.length;
    const current = state.currentIndex + 1;
    $('#mcr-progress').textContent = `${current} / ${total}`;
  }

  // ===== UI 更新 =====
  function updateUI() {
    // 模式按钮
    $$('[data-mode]').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.mode === state.mode);
    });

    // RTL按钮
    $$('[data-rtl]').forEach(btn => {
      btn.classList.toggle('active', (btn.dataset.rtl === 'true') === state.rtl);
    });

    // 适配按钮
    $$('[data-fit]').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.fit === state.dualFit);
    });

    // 显示/隐藏设置组
    $('#mcr-dual-settings').style.display = state.mode === 'dual' ? 'block' : 'none';
    $('#mcr-vertical-settings').style.display = state.mode === 'vertical' ? 'block' : 'none';

    // 双页适配类
    const container = $('#mcr-dual-container');
    container.classList.toggle('fit-height', state.dualFit === 'height');
    container.classList.toggle('fit-width', state.dualFit === 'width');
  }

  // ===== 启动 =====
  async function boot() {
    if (!/\/m\d+\/?$/.test(location.pathname)) return;

    createUI();
    bindEvents();

    const imgs = await collectImages();
    if (!imgs.length) {
      alert('未能获取到章节图片,请刷新重试');
      return;
    }

    state.pages = imgs;
    updateUI();
    render();
  }

  setTimeout(boot, 0);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址