GitHub AI Reader Redirector (Tampermonkey Version)

在 GitHub 页面添加悬浮按钮和快捷键,一键在新标签页中打开 ZRead.ai 或 DeepWiki 进行 AI 项目解析。

スクリプトをインストールするには、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         GitHub AI Reader Redirector (Tampermonkey Version)
// @namespace    https://zread.ai/
// @version      1.0.0
// @description  在 GitHub 页面添加悬浮按钮和快捷键,一键在新标签页中打开 ZRead.ai 或 DeepWiki 进行 AI 项目解析。
// @author       Andrew05060414
// @match        *://*.github.com/*

// @grant        none

// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';

  // Config targets
  const TARGETS = {
    zread: {
      name: 'ZRead.ai',
      urlTemplate: 'https://zread.ai/{owner}/{repo}'
    },
    deepwiki: {
      name: 'DeepWiki',
      urlTemplate: 'https://deepwiki.com/{owner}/{repo}'
    }
  };

  // State variables
  let repoInfo = null;
  let isDarkMode = false;
  
  // DOM Elements
  let widgetWrapper = null;
  let floatBtn = null;
  let hoverMenu = null;

  // Translation Dictionary
  const userLang = navigator.language || navigator.userLanguage;
  const isZh = userLang.startsWith('zh');
  const t = {
    title: isZh ? 'AI 项目解析' : 'AI Repo Reader',
    newTab: isZh ? '新标签页' : 'New Tab',
    zread: 'ZRead.ai',
    deepwiki: 'DeepWiki',
    dragTip: isZh ? '双击拖动按钮' : 'Double-click to drag'
  };

  // Initialize
  function init() {
    handleUrlChange();

    // Monitor URL changes for GitHub client-side transitions
    let lastUrl = location.href;
    setInterval(() => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        handleUrlChange();
      }
    }, 1000);

    // Sync theme
    detectTheme();
    const themeObserver = new MutationObserver(detectTheme);
    themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-color-mode', 'class'] });

    // Keyboard hotkeys
    document.addEventListener('keydown', (e) => {
      if (!repoInfo) return;
      
      // Ignore keypress inside input/textarea fields
      const activeEl = document.activeElement;
      if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) {
        return;
      }

      // Alt + Z (ZRead.ai)
      if (e.altKey && e.key.toLowerCase() === 'z') {
        openTarget('zread');
        e.preventDefault();
      }
      
      // Alt + D (DeepWiki)
      if (e.altKey && e.key.toLowerCase() === 'd') {
        openTarget('deepwiki');
        e.preventDefault();
      }
    });
  }

  // Handle URL change
  function handleUrlChange() {
    repoInfo = parseGitHubUrl(location.href);
    if (repoInfo) {
      createWidget();
      if (widgetWrapper) widgetWrapper.style.display = 'flex';
    } else {
      if (widgetWrapper) widgetWrapper.style.display = 'none';
    }
  }

  // Check GitHub Theme
  function detectTheme() {
    const colorMode = document.documentElement.getAttribute('data-color-mode');
    const hasDarkClass = document.documentElement.classList.contains('dark');
    isDarkMode = (colorMode === 'dark' || hasDarkClass || (colorMode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches));

    if (widgetWrapper) {
      if (isDarkMode) {
        widgetWrapper.classList.remove('theme-light');
        widgetWrapper.classList.add('theme-dark');
      } else {
        widgetWrapper.classList.remove('theme-dark');
        widgetWrapper.classList.add('theme-light');
      }
    }
  }

  // Parse GitHub repo owner and name
  function parseGitHubUrl(urlStr) {
    try {
      const url = new URL(urlStr);
      const parts = url.pathname.split('/').filter(Boolean);
      if (parts.length >= 2) {
        const owner = parts[0];
        const repo = parts[1];
        const ignoredPaths = [
          'settings', 'marketplace', 'explore', 'notifications', 
          'trending', 'topics', 'sponsors', 'pricing', 'features', 
          'customer-stories', 'about', 'readme', 'search', 'pulls', 'issues'
        ];
        if (ignoredPaths.includes(owner.toLowerCase())) {
          return null;
        }
        return { owner, repo };
      }
    } catch (e) {
      console.error(e);
    }
    return null;
  }

  // Create UI Widget
  function createWidget() {
    if (document.getElementById('gh-ai-reader-userscript-root')) return;

    // Create wrapper container
    widgetWrapper = document.createElement('div');
    widgetWrapper.id = 'gh-ai-reader-userscript-root';
    widgetWrapper.className = 'ai-widget-wrapper';
    
    // Inject Custom CSS
    const styleEl = document.createElement('style');
    styleEl.textContent = `
      .ai-widget-wrapper {
        position: fixed;
        right: 20px;
        bottom: 45%; /* Centered vertically by default */
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 8px;
        z-index: 999999;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        box-sizing: border-box;
      }

      /* Hover Menu */
      .ai-hover-menu {
        width: 170px;
        border-radius: 12px;
        padding: 5px;
        display: flex;
        flex-direction: column;
        gap: 3px;
        opacity: 0;
        transform: translateY(10px) scale(0.95);
        transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
        pointer-events: none;
        box-sizing: border-box;
        border-width: 1px;
        border-style: solid;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
      }

      /* Hover Bridge to prevent menu closing when moving cursor */
      .ai-hover-bridge {
        position: absolute;
        bottom: 35px;
        height: 25px;
        left: 0;
        right: 0;
        background: transparent;
        pointer-events: auto;
        display: none;
      }

      .ai-widget-wrapper:hover .ai-hover-menu {
        opacity: 1;
        transform: translateY(0) scale(1);
        pointer-events: auto;
      }
      
      .ai-widget-wrapper:hover .ai-hover-bridge {
        display: block;
      }

      .menu-header {
        font-size: 10px;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        padding: 5px 8px 3px;
      }

      .menu-item {
        display: flex;
        align-items: center;
        padding: 7px 8px;
        border-radius: 8px;
        cursor: pointer;
        font-size: 12px;
        transition: background 0.2s;
        text-decoration: none;
        box-sizing: border-box;
        gap: 6px;
      }

      .menu-item .badge {
        margin-left: auto;
        font-size: 9px;
        padding: 1px 4px;
        border-radius: 4px;
        font-weight: 500;
      }

      /* Floating trigger button */
      .ai-float-btn {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        cursor: grab;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        border-style: solid;
        border-width: 1px;
        padding: 0;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      }

      .ai-float-btn:hover {
        transform: scale(1.08);
      }

      .ai-float-btn:active {
        cursor: grabbing;
        transform: scale(0.95);
      }

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

      /* Theme styling */
      .theme-dark .ai-hover-menu {
        background: rgba(13, 17, 23, 0.85);
        border-color: rgba(240, 246, 252, 0.15);
      }
      .theme-dark .menu-header {
        color: #8b949e;
      }
      .theme-dark .menu-item {
        color: #f0f6fc;
      }
      .theme-dark .menu-item:hover {
        background: rgba(255, 255, 255, 0.08);
      }
      .theme-dark .menu-item .badge {
        background: rgba(59, 130, 246, 0.15);
        color: #3b82f6;
      }
      .theme-dark .ai-float-btn {
        background: linear-gradient(135deg, #a855f7 0%, #3b82f6 100%);
        border-color: rgba(240, 246, 252, 0.15);
        color: #ffffff;
        box-shadow: 0 4px 16px rgba(147, 51, 234, 0.3);
      }

      .theme-light .ai-hover-menu {
        background: rgba(255, 255, 255, 0.88);
        border-color: rgba(208, 215, 222, 0.5);
      }
      .theme-light .menu-header {
        color: #57606a;
      }
      .theme-light .menu-item {
        color: #24292f;
      }
      .theme-light .menu-item:hover {
        background: rgba(0, 0, 0, 0.05);
      }
      .theme-light .menu-item .badge {
        background: rgba(37, 99, 235, 0.1);
        color: #2563eb;
      }
      .theme-light .ai-float-btn {
        background: linear-gradient(135deg, #9333ea 0%, #2563eb 100%);
        border-color: rgba(208, 215, 222, 0.5);
        color: #ffffff;
        box-shadow: 0 4px 16px rgba(37, 99, 235, 0.25);
      }
    `;

    // Load persisted coordinates
    const savedBottom = localStorage.getItem('gh-ai-reader-btn-bottom');
    if (savedBottom) {
      widgetWrapper.style.bottom = savedBottom;
    }

    // Build DOM structure
    widgetWrapper.innerHTML = `
      <div class="ai-hover-menu">
        <div class="ai-hover-bridge"></div>
        <div class="menu-header">${t.title}</div>
        <div class="menu-item" id="menu-zread">
          <span>📖</span>
          <span>${t.zread}</span>
          <span class="badge">${t.newTab}</span>
        </div>
        <div class="menu-item" id="menu-deepwiki">
          <span>🧠</span>
          <span>${t.deepwiki}</span>
          <span class="badge">${t.newTab}</span>
        </div>
      </div>
      <button class="ai-float-btn" title="${t.dragTip}">
        <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path d="M12 2L14.85 9.15L22 12L14.85 14.85L12 22L9.15 14.85L2 12L9.15 9.15L12 2Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
          <path d="M19 4L20 6.5L22.5 7.5L20 8.5L19 11L18 8.5L15.5 7.5L18 6.5L19 4Z" fill="currentColor" opacity="0.65"/>
        </svg>
      </button>
    `;

    document.body.appendChild(styleEl);
    document.body.appendChild(widgetWrapper);

    // Dynamic theme setup
    detectTheme();

    // Map elements
    floatBtn = widgetWrapper.querySelector('.ai-float-btn');
    hoverMenu = widgetWrapper.querySelector('.ai-hover-menu');

    // Click behavior
    floatBtn.addEventListener('click', (e) => {
      if (hasDragged) {
        e.preventDefault();
        return;
      }
      openTarget('zread'); // Default target is ZRead
    });

    // Hover Menu actions
    widgetWrapper.querySelector('#menu-zread').addEventListener('click', () => openTarget('zread'));
    widgetWrapper.querySelector('#menu-deepwiki').addEventListener('click', () => openTarget('deepwiki'));

    // Draggable functionality
    let isDragging = false;
    let startY = 0;
    let startBottom = 0;
    let hasDragged = false;
    const dragThreshold = 5;

    floatBtn.addEventListener('mousedown', (e) => {
      isDragging = true;
      startY = e.clientY;
      startBottom = parseInt(window.getComputedStyle(widgetWrapper).bottom);
      widgetWrapper.style.transition = 'none';
      hasDragged = false;
      e.preventDefault();
    });

    document.addEventListener('mousemove', (e) => {
      if (!isDragging) return;
      const diff = startY - e.clientY;
      if (Math.abs(diff) > dragThreshold) {
        hasDragged = true;
      }
      const newBottom = startBottom + diff;
      const maxBottom = window.innerHeight - 100;
      const minBottom = 20;
      
      const computedBottom = `${Math.min(maxBottom, Math.max(minBottom, newBottom))}px`;
      widgetWrapper.style.bottom = computedBottom;
    });

    document.addEventListener('mouseup', () => {
      if (isDragging) {
        isDragging = false;
        widgetWrapper.style.transition = 'bottom 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
        // Save position
        localStorage.setItem('gh-ai-reader-btn-bottom', widgetWrapper.style.bottom);
      }
    });
  }

  // Action: Open reader URL in a new tab
  function openTarget(key) {
    if (!repoInfo) return;
    const template = TARGETS[key]?.urlTemplate;
    if (template) {
      const url = template.replace('{owner}', repoInfo.owner).replace('{repo}', repoInfo.repo);
      window.open(url, '_blank');
    }
  }

  // Start
  init();
})();