// ==UserScript== // @name GitHub镜像站增强工具 // @namespace https://github.com/GamblerIX/ // @description 汉化GitHub镜像站界面、加速下载、自动跳转到镜像站 // @icon https://github.githubassets.com/pinned-octocat.svg // @version 3.0.3 // @author GamblerIX // @license MIT // @homepage https://github.com/GamblerIX/GithubProxy // @supportURL https://github.com/GamblerIX/GithubProxy/issues // @match https://github.com/* // @match https://hub.mihoyo.online/* // @match https://skills.github.com/* // @match https://gist.github.com/* // @match https://gist.mihoyo.online/* // @match https://education.github.com/* // @match https://www.githubstatus.com/* // @require https://update.greasyfork.icu/scripts/461072/1661491/GitHub%20%E4%B8%AD%E6%96%87.js // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_notification // @grant GM_setClipboard // @grant GM_openInTab // @grant window.onurlchange // @connect fanyi.iflyrec.com // @noframes // @downloadURL https://update.greasyfork.icu/scripts/550044/GitHub%E9%95%9C%E5%83%8F%E7%AB%99%E5%A2%9E%E5%BC%BA%E5%B7%A5%E5%85%B7.user.js // @updateURL https://update.greasyfork.icu/scripts/550044/GitHub%E9%95%9C%E5%83%8F%E7%AB%99%E5%A2%9E%E5%BC%BA%E5%B7%A5%E5%85%B7.meta.js // ==/UserScript== (function () { 'use strict'; // ==================== 配置常量 ==================== const CONFIG = { VERSION: '3.0.3', LANG: 'zh-CN', // 存储键名 STORAGE_KEYS: { AUTO_REDIRECT: 'auto_redirect', ENABLE_TRANSLATION: 'enable_translation', RAW_FAST_INDEX: 'xiu2_menu_raw_fast', RAW_DOWN_LINK: 'menu_rawDownLink', GIT_CLONE: 'menu_gitClone', }, // 性能配置 PERFORMANCE: { MAX_TEXT_LENGTH: 500, DEBOUNCE_DELAY: 300, CACHE_EXPIRE_TIME: 5 * 60 * 1000, REQUEST_TIMEOUT: 10000, }, // CSS选择器 SELECTORS: { RELEASE_FOOTER: '.Box-footer', RAW_BUTTON: 'a[data-testid="raw-button"]', FILE_ICONS: 'div.Box-row svg.octicon.octicon-file, .react-directory-filename-column>svg.color-fg-muted', }, // CSS类名 CSS_CLASSES: { XIU2_RS: 'XIU2-RS', XIU2_RF: 'XIU2-RF', FILE_DOWNLOAD_LINK: 'fileDownLink', TRANSLATE_BUTTON: 'translate-me', } }; // ==================== 下载源配置 ==================== const DOWNLOAD_SOURCES = { release: [ ['https://releases.mihoyo.online/https://github.com', '自建', 'CF CDN - 自建加速源'], ['https://github.com', '官方', 'GitHub 官方源'] ], clone: [ ['https://gitclone.com', 'GitClone', '大陆推荐,首次慢,缓存后较快'], ['https://github.com', '官方', 'GitHub 官方源'] ], ssh: [ ['ssh://git@ssh.github.com:443/', '官方SSH', 'GitHub 官方 SSH 443端口'] ], raw: [ ['https://raw.mihoyo.online', '自建', 'CF CDN - 自建加速源'], ['https://raw.githubusercontent.com', '官方', 'GitHub 官方源'] ] }; // ==================== 工具函数 ==================== const Utils = { // 防抖函数 debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, // 安全DOM查询 safeQuerySelector(selector, parent = document) { try { return parent.querySelector(selector); } catch (error) { console.warn(`[GitHub增强] 查询选择器失败: ${selector}`, error); return null; } }, // 安全DOM查询(多个) safeQuerySelectorAll(selector, parent = document) { try { return parent.querySelectorAll(selector); } catch (error) { console.warn(`[GitHub增强] 查询选择器失败: ${selector}`, error); return []; } }, // 创建元素 createElement(tag, attributes = {}, textContent = '') { const element = document.createElement(tag); Object.entries(attributes).forEach(([key, value]) => { if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else { element.setAttribute(key, value); } }); if (textContent) element.textContent = textContent; return element; }, // 获取嵌套属性 getNestedProperty(obj, path) { return path.split('.').reduce((acc, part) => { const match = part.match(/(\w+)(?:\[(\d+)\])?/); if (!match) return undefined; const key = match[1]; const index = match[2]; if (acc && acc[key] !== undefined) { return index !== undefined ? acc[key][index] : acc[key]; } return undefined; }, obj); } }; // ==================== 存储管理 ==================== const Storage = { get(key, defaultValue = null) { try { return GM_getValue(key, defaultValue); } catch (error) { console.warn(`[GitHub增强] 获取存储失败: ${key}`, error); return defaultValue; } }, set(key, value) { try { GM_setValue(key, value); return true; } catch (error) { console.warn(`[GitHub增强] 设置存储失败: ${key}`, error); return false; } } }; // ==================== 缓存管理 ==================== class SimpleCache { constructor() { this.cache = new Map(); this.expireTime = CONFIG.PERFORMANCE.CACHE_EXPIRE_TIME; } set(key, value) { this.cache.set(key, { value, expire: Date.now() + this.expireTime }); } get(key) { const item = this.cache.get(key); if (!item) return null; if (Date.now() > item.expire) { this.cache.delete(key); return null; } return item.value; } clear() { this.cache.clear(); } } // ==================== 主应用类 ==================== class GitHubEnhancer { constructor() { this.cache = new SimpleCache(); this.pageConfig = {}; this.rawFastIndex = Storage.get(CONFIG.STORAGE_KEYS.RAW_FAST_INDEX, 0); // 初始化默认设置 this.initDefaultSettings(); } // 初始化默认设置 initDefaultSettings() { const defaults = { [CONFIG.STORAGE_KEYS.AUTO_REDIRECT]: true, [CONFIG.STORAGE_KEYS.ENABLE_TRANSLATION]: true, [CONFIG.STORAGE_KEYS.RAW_FAST_INDEX]: 0, [CONFIG.STORAGE_KEYS.RAW_DOWN_LINK]: true, [CONFIG.STORAGE_KEYS.GIT_CLONE]: true, }; Object.entries(defaults).forEach(([key, defaultValue]) => { if (Storage.get(key) === null) { Storage.set(key, defaultValue); } }); } // 初始化应用 async init() { try { console.log(`[GitHub增强] v${CONFIG.VERSION} 开始初始化...`); // 检查自动跳转 if (this.shouldRedirect()) { this.performRedirect(); return; } // 检查依赖 if (!this.checkDependencies()) { return; } // 初始化功能 this.setupLanguageEnvironment(); this.updatePageConfig(); this.setupEventListeners(); this.registerMenuCommands(); this.setupColorMode(); // 延迟执行的功能 setTimeout(() => this.addRawFile(), 1000); setTimeout(() => this.addRawDownLink(), 2000); // 检查Release页面 if (location.pathname.indexOf('/releases') > -1) { setTimeout(() => this.addRelease(), 1500); } // 执行初始翻译 this.performInitialTranslation(); console.log(`[GitHub增强] 初始化完成`); } catch (error) { console.error('[GitHub增强] 初始化失败:', error); this.showNotification('初始化失败,请刷新页面重试'); } } // 检查是否需要重定向 shouldRedirect() { return Storage.get(CONFIG.STORAGE_KEYS.AUTO_REDIRECT) && window.location.host === 'github.com'; } // 执行重定向 performRedirect() { const newUrl = window.location.href.replace('https://github.com', 'https://hub.mihoyo.online'); console.log(`[GitHub增强] 重定向到: ${newUrl}`); window.location.replace(newUrl); } // 检查依赖 checkDependencies() { if (typeof I18N === 'undefined') { this.showNotification('词库文件未加载,脚本无法运行!'); return false; } console.log('[GitHub增强] 词库文件已加载'); return true; } // 设置语言环境 setupLanguageEnvironment() { document.documentElement.lang = CONFIG.LANG; new MutationObserver(() => { if (document.documentElement.lang === "en") { document.documentElement.lang = CONFIG.LANG; } }).observe(document.documentElement, { attributeFilter: ['lang'] }); } // 更新页面配置 updatePageConfig() { const pageType = this.detectPageType(); if (pageType) { this.pageConfig = this.buildPageConfig(pageType); } } // 检测页面类型 detectPageType() { try { const url = new URL(window.location.href); const { hostname, pathname } = url; const pageMap = { 'gist.github.com': 'gist1', 'www.githubstatus.com': 'status', 'skills.github.com': 'skills', 'education.github.com': 'education', 'gist.mihoyo.online': 'gist2', }; const site = pageMap[hostname] || 'github'; const specialSites = ['gist1', 'status', 'skills', 'education', 'gist2']; if (specialSites.includes(site)) { return site; } // 简化的页面类型检测 if (pathname === '/') { return document.body?.classList.contains("logged-in") ? 'dashboard' : 'homepage'; } else if (pathname.includes('/releases')) { return 'repository'; } else { return 'repository'; // 默认为仓库页面 } } catch (error) { console.warn('[GitHub增强] 页面类型检测失败:', error); return 'repository'; } } // 构建页面配置 buildPageConfig(pageType) { try { return { currentPageType: pageType, staticDict: { ...I18N[CONFIG.LANG].public.static, ...(I18N[CONFIG.LANG][pageType]?.static || {}) }, regexpRules: [ ...(I18N[CONFIG.LANG][pageType]?.regexp || []), ...I18N[CONFIG.LANG].public.regexp ] }; } catch (error) { console.warn('[GitHub增强] 构建页面配置失败:', error); return { currentPageType: pageType }; } } // 设置事件监听 setupEventListeners() { // URL变化监听 if (window.onurlchange === undefined) { this.addUrlChangeEvent(); } window.addEventListener('urlchange', () => { this.setupColorMode(); if (location.pathname.indexOf('/releases') > -1) { this.addRelease(); } setTimeout(() => this.addRawFile(), 1000); setTimeout(() => this.addRawDownLink(), 2000); }); // Turbo事件监听 document.addEventListener('turbo:load', () => { this.translateTitle(); }); // 设置DOM变化监听器 this.setupMutationObserver(); } // 设置DOM变化监听器 setupMutationObserver() { const observer = new MutationObserver((mutations) => { this.handleMutations(mutations); }); const config = { childList: true, subtree: true, characterData: true }; if (document.body) { observer.observe(document.body, config); } else { document.addEventListener('DOMContentLoaded', () => { if (document.body) { observer.observe(document.body, config); } }); } } // 处理DOM变化 handleMutations(mutations) { // 处理Release页面的动态加载 if (location.pathname.indexOf('/releases') > -1) { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'DIV' && node.dataset && node.dataset.viewComponent === 'true' && node.classList && node.classList.contains('Box')) { setTimeout(() => this.addRelease(), 100); break; } } } } } // 处理仓库页面的Git Clone等功能 if (Utils.safeQuerySelector('#repository-container-header:not([hidden])')) { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'DIV') { if (node.parentElement && node.parentElement.id === '__primerPortalRoot__') { // 这里可以添加Git Clone相关的处理 setTimeout(() => { this.addDownloadZIP(node); this.addGitClone(node); }, 100); } } } } } // 处理翻译 if (Storage.get(CONFIG.STORAGE_KEYS.ENABLE_TRANSLATION)) { const nodesToTranslate = mutations.flatMap(({ addedNodes, type }) => { if (type === 'childList' && addedNodes.length > 0) { return [...addedNodes].filter(node => node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE ); } return []; }); nodesToTranslate.forEach(node => { this.traverseNode(node); }); } } // 添加URL变化事件 addUrlChangeEvent() { const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function (...args) { const result = originalPushState.apply(this, args); window.dispatchEvent(new Event('urlchange')); return result; }; history.replaceState = function (...args) { const result = originalReplaceState.apply(this, args); window.dispatchEvent(new Event('urlchange')); return result; }; window.addEventListener('popstate', () => { window.dispatchEvent(new Event('urlchange')); }); } // 执行初始翻译 performInitialTranslation() { if (Storage.get(CONFIG.STORAGE_KEYS.ENABLE_TRANSLATION)) { this.translateTitle(); this.traverseNode(document.body); } } // 翻译页面标题 translateTitle() { if (!Storage.get(CONFIG.STORAGE_KEYS.ENABLE_TRANSLATION)) return; try { const text = document.title; const cacheKey = `title_${text}`; let translatedText = this.cache.get(cacheKey); if (translatedText) { document.title = translatedText; return; } translatedText = I18N[CONFIG.LANG]['title']['static'][text] || ''; if (!translatedText) { const rules = I18N[CONFIG.LANG]['title'].regexp || []; for (const [pattern, replacement] of rules) { translatedText = text.replace(pattern, replacement); if (translatedText !== text) break; } } if (translatedText && translatedText !== text) { document.title = translatedText; this.cache.set(cacheKey, translatedText); } } catch (error) { console.warn('[GitHub增强] 标题翻译失败:', error); } } // 遍历并翻译节点 traverseNode(rootNode) { if (!Storage.get(CONFIG.STORAGE_KEYS.ENABLE_TRANSLATION) || !rootNode) return; try { if (rootNode.nodeType === Node.TEXT_NODE) { this.translateTextNode(rootNode); return; } const walker = document.createTreeWalker( rootNode, NodeFilter.SHOW_TEXT, null, false ); let node; while (node = walker.nextNode()) { this.translateTextNode(node); } } catch (error) { console.warn('[GitHub增强] 节点遍历失败:', error); } } // 翻译文本节点 translateTextNode(node) { if (!node.textContent || node.textContent.length > CONFIG.PERFORMANCE.MAX_TEXT_LENGTH) return; const text = node.textContent.trim(); if (!text || /^[\s0-9]*$/.test(text) || /^[\u4e00-\u9fa5]+$/.test(text)) return; const translatedText = this.translateText(text); if (translatedText && translatedText !== text) { node.textContent = node.textContent.replace(text, translatedText); } } // 翻译文本 translateText(text) { if (!this.pageConfig.staticDict) return false; const cacheKey = `text_${text}`; let translatedText = this.cache.get(cacheKey); if (translatedText) return translatedText; // 静态翻译 translatedText = this.pageConfig.staticDict[text]; if (typeof translatedText === 'string') { this.cache.set(cacheKey, translatedText); return translatedText; } // 正则翻译 if (this.pageConfig.regexpRules) { for (const [pattern, replacement] of this.pageConfig.regexpRules) { try { translatedText = text.replace(pattern, replacement); if (translatedText !== text) { this.cache.set(cacheKey, translatedText); return translatedText; } } catch (error) { console.warn('[GitHub增强] 正则翻译失败:', error); } } } return false; } // 设置颜色模式 setupColorMode() { try { let styleElement = document.getElementById('XIU2-Github'); if (!styleElement) { styleElement = Utils.createElement('style', { id: 'XIU2-Github', type: 'text/css' }); document.head.appendChild(styleElement); } let backColor = '#ffffff', fontColor = '#888888'; const rootElement = document.documentElement; if (rootElement.dataset.colorMode === 'dark') { if (rootElement.dataset.darkTheme === 'dark_dimmed') { backColor = '#272e37'; fontColor = '#768390'; } else { backColor = '#161a21'; fontColor = '#97a0aa'; } } styleElement.textContent = `.XIU2-RS a {--XIU2-background-color: ${backColor}; --XIU2-font-color: ${fontColor};}`; } catch (error) { console.warn('[GitHub增强] 颜色模式设置失败:', error); } } // 添加Release加速下载 addRelease() { try { console.log('[GitHub增强] 尝试添加Release加速下载...'); const footers = Utils.safeQuerySelectorAll(CONFIG.SELECTORS.RELEASE_FOOTER); console.log(`[GitHub增强] 找到 ${footers.length} 个 footer 元素`); if (footers.length === 0) { console.log('[GitHub增强] 未找到Release页面的footer元素'); return; } if (location.pathname.indexOf('/releases') === -1) { console.log('[GitHub增强] 当前不在Release页面'); return; } const downloadSources = DOWNLOAD_SOURCES.release; const divDisplay = document.documentElement.clientWidth > 755 ? 'margin-top: -3px;margin-left: 8px;display: inherit;' : 'margin-left: -90px;'; // 添加样式(如果不存在) if (!document.querySelector('#XIU2-release-style')) { const style = Utils.createElement('style', { id: 'XIU2-release-style' }); style.textContent = '@media (min-width: 768px) {.Box-footer li.Box-row>div>span.color-fg-muted {min-width: 27px !important;}}'; document.head.appendChild(style); } let addedCount = 0; footers.forEach(footer => { if (footer.querySelector(`.${CONFIG.CSS_CLASSES.XIU2_RS}`)) { console.log('[GitHub增强] 该footer已存在加速按钮,跳过'); return; } const links = Utils.safeQuerySelectorAll('li.Box-row a', footer); console.log(`[GitHub增强] 在footer中找到 ${links.length} 个下载链接`); links.forEach(link => { const href = link.href.split(location.host); if (href.length < 2) return; let html = `