您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
解决发布说明过长挤占发布列表空间、发布文件过多难定位目标文件的问题,通过优化说明界面和增加筛选功能,实现需求文件的快速定位,提升浏览体验。
// ==UserScript== // @name GitHub Releases 浏览优化 // @name:zh-CN GitHub Releases 浏览优化 // @name:en GitHub Releases Navigation Enhancer // @namespace https://github.com/wha4up // @icon data:image/webp;base64,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 // @version 2.0.2 // @description 解决发布说明过长挤占发布列表空间、发布文件过多难定位目标文件的问题,通过优化说明界面和增加筛选功能,实现需求文件的快速定位,提升浏览体验。 // @description:zh-CN 解决发布说明过长挤占发布列表空间、发布文件过多难定位目标文件的问题,通过优化说明界面和增加筛选功能,实现需求文件的快速定位,提升浏览体验。 // @description:en Tackles the issues of lengthy release notes taking up too much space and the difficulty of locating specific files among numerous assets. By optimizing the notes interface and adding powerful filtering features, it enables quick location of desired files and enhances the browsing experience. // @author eecopilot,wha4up(with assistance from Gemini) // @match https://github.com/*/*/releases // @match https://github.com/*/*/releases/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @homepage https://github.com/wha4up/GitHub-Releases-Navigation-Enhancer // @supportURL https://github.com/wha4up/GitHub-Releases-Navigation-Enhancer/issues // @license MIT // ==/UserScript== /* * This script is a modified version of 'github-release-platform-filter' (version 0.3.3) * Originally created by eecopilot. * Original project: https://github.com/eecopilot/github-release-platform-filter * * Modifications and additions by wha4up and Gemini. * Copyright (c) 2025 wha4up. * * This entire work (including original and modifications) is licensed under the MIT License. * * MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (function () { 'use strict'; let navigationReInit; // ================================================================================= // [模块] 点击外部关闭处理器 (Click-Outside Handler) // ================================================================================= const ReleaseNotesCollapser = class { constructor() { this.EXCLUDE_SELECTORS = [ '.AppHeader', '.mb-5', '.Box', `.${GithubReleaseEnhancer.config.CLASS_NAMES.SETTINGS_OVERLAY}`, ].join(','); this.activePanel = null; this.boundClickHandler = null; this.lastTapTime = 0; } initForPanel(panel) { if (!GithubReleaseEnhancer.store.state.clickOutsideToCollapse || this.activePanel) { return; } this.activePanel = panel; if (!this.boundClickHandler) { this.boundClickHandler = this.handleDocumentClick.bind(this); setTimeout(() => { document.addEventListener('click', this.boundClickHandler, { capture: true }); document.addEventListener('touchend', this.boundClickHandler, { capture: true }); }, 50); } } handleDocumentClick(event) { if (!this.activePanel) return; if (event.type === 'touchend') { const now = Date.now(); if (now - this.lastTapTime < 300) { event.preventDefault(); return; } this.lastTapTime = now; } const target = event.target; if (this.activePanel.contains(target) || target.closest(this.EXCLUDE_SELECTORS)) { return; } this.collapsePanel(); } collapsePanel() { if (!this.activePanel) return; const toggleBtn = this.activePanel.querySelector(`.${GithubReleaseEnhancer.config.CLASS_NAMES.TOGGLE_BUTTON}`); if (toggleBtn) { toggleBtn.click(); } else { this.activePanel.classList.remove(GithubReleaseEnhancer.config.CLASS_NAMES.EXPANDED); this.cleanup(); } } cleanup() { if (this.boundClickHandler) { document.removeEventListener('click', this.boundClickHandler, { capture: true }); document.removeEventListener('touchend', this.boundClickHandler, { capture: true }); this.boundClickHandler = null; } this.activePanel = null; } }; // ================================================================================= // [模块] 状态管理器 (State Manager) // ================================================================================= const shallowEqual = (objA, objB) => { if (Object.is(objA, objB)) return true; if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) return false; for (let i = 0; i < keysA.length; i++) { const key = keysA[i]; if (!Object.prototype.hasOwnProperty.call(objB, key)) { return false; } const valA = objA[key]; const valB = objB[key]; if (valA instanceof Set && valB instanceof Set) { if (valA.size !== valB.size) return false; for (const item of valA) { if (!valB.has(item)) return false; } continue; } if (!Object.is(valA, valB)) return false; } return true; }; const createStore = (initialState) => { let state = { ...initialState }; const listeners = new Set(); let isNotifying = false; let hasPendingUpdate = false; const notify = () => { if (isNotifying) { hasPendingUpdate = true; return; } isNotifying = true; listeners.forEach(listener => listener(state)); isNotifying = false; if (hasPendingUpdate) { hasPendingUpdate = false; queueMicrotask(notify); } }; const setState = (update) => { const nextState = { ...state, ...update }; if (shallowEqual(state, nextState)) { return; } state = nextState; notify(); }; const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); }; return { get state() { return state; }, setState, subscribe }; }; const GithubReleaseEnhancer = { store: null, _observer: null, _globalClickHandler: null, _unsubscribe: null, _isInitialized: false, assetCache: new Map(), notesCollapser: null, // ================================================================================= // [模块] 配置 (Config) // ================================================================================= config: { RELEASE_NOTES_MAX_HEIGHT_DEFAULT: 300, DEBOUNCE_DELAY: 200, POPUP_LEAVE_CLOSE_DELAY: 300, STORAGE_KEY_SETTINGS: 'ghre_user_settings_v3.8', REGEX_CACHE_SIZE: 100, texts: { filterButton: { zh: '筛选', en: 'Filter' }, expandButton: { zh: '展开 ▾', en: 'Expand ▾' }, collapseButton: { zh: '收起 ▴', en: 'Collapse ▴' }, settingsPanelTitle: { zh: '脚本设置 - GitHub Releases 优化', en: 'Settings - GitHub Releases Enhancer' }, settingsSaveButton: { zh: '保存设置', en: 'Save Settings' }, settingsCancelButton: { zh: '取消', en: 'Cancel' }, settingsMaxHeightLabel: { zh: '发布说明默认最大高度 (px):', en: 'Release Notes Default Max Height (px):' }, settingsHiddenRuleLabel: { zh: "自定义隐藏关键字 (.开头为后缀, 否则为关键词, 词组需加''):", en: "Custom Hidden Keywords (. for suffix, keyword otherwise, use '' for phrases):" }, settingsHiddenRulePlaceholder: { zh: "例如: .sig, debug, 'source code'", en: "e.g.: .sig, debug, 'source code'" }, settingsClickOutsideLabel: { zh: '点击页面空白处收起发布说明', en: 'Click outside to collapse release notes' }, platformFilterTitle: { zh: "平台筛选", en: "Platform Filters" }, supplementaryFilterTitle: { zh: "补充筛选", en: "Supplementary Filters" }, selectAllLabel: { zh: '全选', en: 'Select All' }, deselectAllLabel: { zh: '清空', en: 'Clear' }, restoreLabel: { zh: '恢复', en: 'Restore' }, hiddenAssetCountTitle: { zh: "隐藏的资产数量", en: "Number of hidden assets" }, activeFilterCountTitle: { zh: '已激活筛选规则数量', en: 'Number of active filters' }, keywordLabel: { zh: '关键字', en: 'Keywords' }, archLabel: { zh: '架构', en: 'Architecture' }, allArchLabel: { zh: '所有', en: 'all' }, langLabel: { zh: '语言', en: 'Language' }, resLabel: { zh: '分辨率', en: 'Resolution' }, sourceCodeLabel: { zh: '源码', en: 'Source Code' }, settingsMenuName: { zh: 'GitHub Releases 优化设置', en: 'GitHub Releases Enhancer Settings' }, filterModeLabel: { zh: '默认筛选模式', en: 'Default Filter Mode' }, intelligentFilterLabel: { zh: '智能筛选', en: 'Intelligent Filter' }, preferredFilterLabel: { zh: '偏好筛选', en: 'Preferred Filter' }, savePrefsButton: { zh: '保存当前筛选为偏好', en: 'Save current filters as preference' }, prefsSavedAlert: { zh: '偏好已保存!', en: 'Preferences saved!' }, settingsResetButton: { zh: '重置设置', en: 'Reset Settings' }, resetConfirmationTitle: { zh: '确认重置', en: 'Confirm Reset' }, resetConfirmationMessage: { zh: '您确定要将所有设置恢复为默认值吗?此操作不可撤销,并将清空您保存的偏好筛选。', en: 'Are you sure you want to reset all settings to their default values? This action cannot be undone and will clear your saved filter preferences.' }, resetConfirmButton: { zh: '确认重置', en: 'Confirm Reset' }, }, PLATFORMS: [ { id: 'windows', name: 'Windows', exclusiveFormats: ['.exe', '.msi', '.msix'] }, { id: 'macos', name: 'MacOS', exclusiveFormats: ['.dmg', '.pkg'] }, { id: 'linux', name: 'Linux', exclusiveFormats: ['.deb', '.rpm', '.appimage'] }, { id: 'android', name: 'Android', exclusiveFormats: ['.apk', '.aab'] }, { id: 'ios', name: 'iOS', exclusiveFormats: ['.ipa'] }, { id: 'other_os', name: '未匹配', exclusiveFormats: [] } ], ARCH_TAGS_CONFIG: [ { id: 'x64', name: 'x64' }, { id: 'x86', name: 'x86' }, { id: 'arm64', name: 'ARM64' }, { id: 'arm', name: 'ARM' }, { id: 'oth', name: 'OTH' } ], SOURCE_CODE_KEYWORDS: ['source code (zip)', 'source code (tar.gz)'], HIDDEN_KEYWORDS_DEFAULT: [ '.blockmap', '.rmp', '.sig', '.asc', '.sha256', '.md5', 'sha1', 'sha512', '.pdb', '.sym', '.debug', '.map', '.symbols', '.dSYM' ], AGNOSTIC_CONTAINERS: ['.zip', '.7z', '.tar.gz', '.rar', '.tar', '.gz', '.bz2', '.xz', '.tar.xz', '.tar.bz2', '.tar.lz', '.pkg.zst', '.app.tar', '.flatpak'], LANGUAGES: { 'zh-hans': { name: '简体中文', keywords: ['zh-cn', 'zh-hans', 'chinese simplified', 'chs', '简体', '简中'] }, 'zh-hant': { name: '繁體中文', keywords: ['zh-tw', 'zh-hk', 'zh-hant', 'chinese traditional', 'cht', '繁體', '正體', '繁中'] }, 'en': { name: 'English', keywords: ['en', 'english', 'eng'] }, 'fr': { name: 'Français', keywords: ['fr', 'french', 'français', 'fra'] }, 'de': { name: 'Deutsch', keywords: ['de', 'german', 'deutsch', 'ger', 'deu'] }, 'ru': { name: 'Русский', keywords: ['ru', 'russian', 'русский', 'rus'] }, 'ja': { name: '日本語', keywords: ['ja', 'japanese', '日本語', 'jpn', 'nihongo'] }, 'ko': { name: '한국어', keywords: ['ko', 'korean', '한국어', 'kor', 'hanguk'] }, 'it': { name: 'Italiano', keywords: ['it', 'italian', 'italiano', 'ita'] }, 'es': { name: 'Español', keywords: ['es', 'spanish', 'español', 'spa'] }, 'pt': { name: 'Português', keywords: ['pt', 'portuguese', 'português', 'por'] }, 'ar': { name: 'العربية', keywords: ['ar', 'arabic', 'العربية', 'ara'] } }, RESOLUTIONS: { 'hd': { name: 'HD', keywords: ['hd', '720p', '1080p', '1280x720', '1920x1080', 'fhd', '2k', '1440p', 'qhd', '4k', '2160p', 'uhd'] }, 'sd': { name: '常规', keywords: ['sd', '480p', '360p', 'standard definition'] } }, CLASS_NAMES: { MARKDOWN_BODY: 'markdown-body', RELEASE_NOTES_PANEL: 'my-3', TOGGLE_BUTTON: 'toggle-button', EXPANDED: 'expanded', FILTER_CONTAINER: 'ghre-filter-interaction-wrapper', FILTER_BUTTON: 'ghre-filter-button', PLATFORM_OPTIONS: 'ghre-platform-options', PLATFORM_OPTIONS_INNER: 'ghre-platform-options-inner', PLATFORM_OPTIONS_GRID: 'ghre-options-grid', PLATFORM_OPTIONS_OPEN: 'ghre-dropdown-open', PLATFORM_OPTION: 'ghre-platform-option', ARCH_TAG_CONTAINER: 'ghre-arch-tag-container', ARCH_TAG: 'ghre-arch-tag', KEYWORD_TOGGLE_OPTION: 'ghre-keyword-toggle-option', OPTIONS_SEPARATOR: 'ghre-options-separator', OPTIONS_TITLE: 'ghre-options-title', SELECTED: 'selected', HIDDEN_ASSET: 'hidden-asset', HIDDEN_ASSET_COUNTER: 'ghre-hidden-asset-counter', SUMMARY_MARKER_ICON: 'ghre-summary-marker-icon', AVAILABILITY_DOT: 'ghre-availability-dot', ACTION_BUTTON: 'ghre-action-button', SETTINGS_PANEL: 'ghre-settings-panel', SETTINGS_OVERLAY: 'ghre-settings-overlay', SETTINGS_TITLE: 'ghre-settings-title', SETTINGS_FORM_GROUP: 'ghre-form-group', SETTINGS_LABEL: 'ghre-settings-label', SETTINGS_INPUT: 'ghre-settings-input', SETTINGS_TEXTAREA: 'ghre-settings-textarea', SETTINGS_ACTIONS: 'ghre-settings-actions', SETTINGS_BUTTON: 'ghre-settings-button', ASSET_LIST_LOADING: 'ghre-assets-loading', POPUP_HEADER: 'ghre-popup-header', POPUP_SETTINGS_BTN: 'ghre-popup-settings-btn', RESET_CONFIRM_DIALOG: 'ghre-reset-confirm-dialog', }, SELECTORS: { ASSET_ROW: '.Box-row, li.Box-row, .release-asset', ASSET_LIST_CONTAINER: '.Box--condensed ul, .release-assets', MAIN_CONTENT: 'main#main-content, main', ASSETS_SUMMARY: 'summary[data-target="details-toggle.summaryTarget"]', ASSET_DOWNLOAD_LINK: 'a[href*="/download/"]', MAIN_REPO_CONTENT: 'main .repository-content', FILTER_COUNTER: '[data-ghre-filter-counter="true"]', HIDDEN_ASSET_COUNTER: '.ghre-hidden-asset-counter', MAIN_ASSET_COUNTER: 'span.Counter:not(.ghre-hidden-asset-counter):not([data-ghre-filter-counter="true"])', SETTINGS_MAX_HEIGHT_INPUT: '#ghreMaxHeightInput', SETTINGS_HIDDEN_RULES_TEXTAREA: '#ghreHiddenRulesTextarea', SETTINGS_CLICK_OUTSIDE_CHECKBOX: '#ghreClickOutsideCheckbox', SETTINGS_SAVE_BUTTON: 'button[data-action="save"]', SETTINGS_CANCEL_BUTTON: 'button[data-action="cancel"]', SETTINGS_RESET_BUTTON: 'button[data-action="reset"]', FIRST_FOCUSABLE_OPTION: '[role="menuitemcheckbox"]', }, }, // ================================================================================= // [模块] 图标 (Icons) // ================================================================================= icons: { filter: `<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-filter mr-2"><path d="M.75 3h14.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1 0-1.5ZM3 7.75A.75.75 0 0 1 3.75 7h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 3 7.75Zm3 4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>`, checkbox_checked: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>`, checkbox_unchecked: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>`, disclosure_triangle_right_svg: `<svg class="octicon octicon-triangle-right" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.427 4.427l3.396 3.396a.25.25 0 010 .354l-3.396 3.396A.25.25 0 016 11.396V4.604a.25.25 0 01.427-.177z"></path></svg>`, gear: `<svg aria-hidden="true" focusable="false" class="octicon octicon-gear" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"><path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644-.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815.806c.081-.08.073-.159-.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113-.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"></path></svg>`, }, // ================================================================================= // [模块] 规则与定义 (Rules & Definitions) // ================================================================================= platformArchRules: { windows: { name: 'Windows', keywords: ['windows', 'win', 'win10', 'win11'], arch: { x64: ['win64', 'x64', 'amd64'], x86: ['win32', 'x86', 'ia32', 'i386', 'i686', '386'], arm64: ['arm64'] } }, macos: { name: 'MacOS', keywords: ['macos', 'osx', 'darwin'], arch: { x64: ['x64', 'amd64', 'intel'], arm64: ['arm64', 'aarch64', 'apple', 'universal'] } }, linux: { name: 'Linux', keywords: ['linux'], arch: { x64: ['x64', 'amd64', 'x86_64'], x86: ['x86', 'i386', 'i686', '386'], arm64: ['arm64', 'aarch64'], arm: ['armv7', 'armhf', 'arm'] , oth:['risc-v', 'riscv', 'powerpc', 'power', 'mips', 'mipsle'] } }, android: { name: 'Android', keywords: ['android'], arch: { arm64: ['arm64', 'arm64-v8a', 'aarch64'], arm: ['arm', 'armeabi-v7a'], x64: ['x64', 'x86_64', 'amd64'], x86: ['x86'] } }, ios: { name: 'iOS', keywords: ['ios'], arch: { arm64: ['arm64', 'aarch64'], arm: ['arm'] } }, other_os: { name: 'Other OS', keywords: ['freebsd', 'netbsd', 'openbsd', 'solaris', 'plan9', 'sunos', 'bsd'] } }, // ================================================================================= // [模块] 工具函数 (Utils) // ================================================================================= utils: { keywordRegexCache: new Map(), maxCacheSize: 100, createElement(tag, options = {}) { try { const el = document.createElement(tag); for (const key in options) { if (key === 'className') { el.className = options[key]; } else if (key === 'textContent') { el.textContent = options[key]; } else if (key === 'innerHTML') { el.innerHTML = options[key]; } else if (key === 'style') { Object.assign(el.style, options[key]); } else if (key === 'dataset') { for (const dataKey in options.dataset) { el.dataset[dataKey] = options.dataset[dataKey]; } } else { el.setAttribute(key, options[key]); } } return el; } catch (error) { console.error(`[GitHub Filter] Failed to create element <${tag}>:`, error); return null; } }, getText(key) { const GRE = GithubReleaseEnhancer; const textObject = GRE.config.texts[key]; if (!textObject) return key; return textObject[GRE.store.state.isChinese ? 'zh' : 'en'] || textObject.en || key; }, escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, getKeywordRegex(kw) { if (this.keywordRegexCache.has(kw)) { const cachedRegex = this.keywordRegexCache.get(kw); this.keywordRegexCache.delete(kw); this.keywordRegexCache.set(kw, cachedRegex); return cachedRegex; } const regex = new RegExp(`(^|[^a-z0-9])${this.escapeRegExp(kw)}([^a-z0-9]|$)`, 'i'); if (this.keywordRegexCache.size >= this.maxCacheSize) { const oldestKey = this.keywordRegexCache.keys().next().value; this.keywordRegexCache.delete(oldestKey); } this.keywordRegexCache.set(kw, regex); return regex; }, checkPageType() { GithubReleaseEnhancer.store.setState({ isMainReleasesPage: /^\/[^/]+\/[^/]+\/releases\/?(?:[?#]|$)/.test(window.location.pathname) }); }, getCurrentPlatform() { try { if (navigator.userAgentData && navigator.userAgentData.platform) { const d = navigator.userAgentData.platform.toLowerCase(); if (d.includes('win')) return 'windows'; if (d.includes('mac')) return 'macos'; if (d.includes('linux')) return 'linux'; if (d.includes('android')) return 'android'; if (d.includes('iphone') || d.includes('ipad') || d.includes('ipod') || d.includes('ios')) return 'ios'; } } catch (e) { /* ignore */ } const p = (navigator.platform || '').toLowerCase(); const u = (navigator.userAgent || '').toLowerCase(); if (p.startsWith('win')) return 'windows'; if (p.startsWith('mac') || p.includes('darwin')) return 'macos'; if (p.startsWith('linux') || p.includes('freebsd')) return 'linux'; if (u.includes('android')) return 'android'; if (u.includes('iphone') || u.includes('ipad') || u.includes('ipod') || p.includes('iphone') || p.includes('ipad') || p.includes('ipod')) return 'ios'; return 'unknown'; }, async getCurrentArchitecture() { try { if (navigator.userAgentData?.getHighEntropyValues) { const d = await navigator.userAgentData.getHighEntropyValues(['architecture', 'bitness']); const a = d.architecture; const b = d.bitness; if (a === 'arm') return b === '64' ? 'arm64' : 'arm'; if (a === 'x86') return b === '64' ? 'x64' : 'x86'; if (a) return a; } } catch (e) { /* ignore */ } const p = (navigator.platform || '').toLowerCase(); const u = (navigator.userAgent || '').toLowerCase(); if (u.includes('arm64') || u.includes('aarch64') || p.includes('arm64') || p.includes('aarch64') || u.includes('armv8')) return 'arm64'; if (u.includes('win64') || u.includes('x64') || u.includes('amd64') || p.includes('64')) return 'x64'; if (u.includes('x86') || p.includes('win32') || p.includes('i386') || p.includes('i686')) { if (!u.includes('x64') && !p.includes('64')) return 'x86'; } if (u.includes('arm') || p.includes('armv7') || p.includes('armhf')) { if (!u.includes('arm64') && !p.includes('arm64')) return 'arm'; } return 'unknown'; }, getCurrentLanguage() { const lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase(); for (const langCode in GithubReleaseEnhancer.config.LANGUAGES) { if (GithubReleaseEnhancer.config.LANGUAGES[langCode].keywords.some(kw => lang.startsWith(kw) || lang === kw)) { return langCode; } } if (lang.startsWith('zh-cn') || lang === 'zh-hans') return 'zh-hans'; if (lang.startsWith('zh-tw') || lang.startsWith('zh-hk') || lang === 'zh-hant') return 'zh-hant'; return lang.split('-')[0]; }, getCurrentResolutionCategory() { const height = window.screen.height; if (height >= 720) return 'hd'; return 'sd'; }, parseAssetInfo(text) { try { const GRE = GithubReleaseEnhancer; const lowerText = text.replace(/\s+/g, ' ').trim().toLowerCase(); const isSourceCode = GRE.config.SOURCE_CODE_KEYWORDS.includes(lowerText); if (isSourceCode) { return { platform: null, architecture: null, language: null, resolution: null, isSourceCode: true, isByKeyword: false }; } if (GRE.core.assetFilter.isHiddenAsset(text, GRE.store.state.hiddenKeywords)) { return { platform: null, architecture: null, language: null, resolution: null, isSourceCode: false, isByKeyword: true }; } let detectedPlatform = null, detectedArch = null, detectedLang = null, detectedRes = null; for (const platform of GRE.config.PLATFORMS) { if (platform.exclusiveFormats.some(ext => lowerText.endsWith(ext))) { detectedPlatform = platform.id; break; } } if (!detectedPlatform) { for (const platformId in GRE.platformArchRules) { const platformRule = GRE.platformArchRules[platformId]; if (platformRule.keywords.some(kw => this.getKeywordRegex(kw).test(lowerText))) { detectedPlatform = platformId; break; } } } const archRules = detectedPlatform ? GRE.platformArchRules[detectedPlatform]?.arch : null; if (archRules) { for (const archKey in archRules) { if (archRules[archKey].some(kw => this.getKeywordRegex(kw).test(lowerText))) { detectedArch = archKey; break; } } } if (!detectedArch) { const generalArchMap = { 'arm64': ['arm64', 'aarch64'], 'x64': ['x64', 'amd64', 'x86_64'], 'x86': ['x86', 'i386', 'i686', '386', 'win32'], 'arm': ['armv7', 'armhf', 'arm'] }; const archDetectionOrder = ['arm64', 'x64', 'x86', 'arm']; for (const arch of archDetectionOrder) { if (generalArchMap[arch].some(kw => { const regex = this.getKeywordRegex(kw); if (kw === 'win32' && (this.getKeywordRegex('x64').test(lowerText) || this.getKeywordRegex('amd64').test(lowerText))) return false; if (arch === 'x86' && kw !== 'win32' && (this.getKeywordRegex('x64').test(lowerText) || this.getKeywordRegex('amd64').test(lowerText))) return false; if (arch === 'arm' && (this.getKeywordRegex('arm64').test(lowerText) || this.getKeywordRegex('aarch64').test(lowerText))) return false; return regex.test(lowerText); })) { detectedArch = arch; break; } } } if (detectedPlatform === 'macos' && (this.getKeywordRegex('apple').test(lowerText) || this.getKeywordRegex('universal').test(lowerText))) detectedArch = 'arm64'; for (const langCode in GRE.config.LANGUAGES) { if (GRE.config.LANGUAGES[langCode].keywords.some(kw => this.getKeywordRegex(kw).test(lowerText))) { detectedLang = langCode; break; } } for (const resCode in GRE.config.RESOLUTIONS) { if (GRE.config.RESOLUTIONS[resCode].keywords.some(kw => this.getKeywordRegex(kw).test(lowerText))) { detectedRes = resCode; break; } } return { platform: detectedPlatform, architecture: detectedArch, language: detectedLang, resolution: detectedRes, isSourceCode: false, isByKeyword: false }; } catch (error) { console.error(`[GitHub Filter@${location.pathname}] Error parsing asset info:`, error, 'Input text:', text); return { platform: null, architecture: null, language: null, resolution: null, isSourceCode: false, isByKeyword: false }; } }, }, // ================================================================================= // [模块] UI 相关 (UI) // ================================================================================= ui: { styles: { commonStylesTemplate() { const GRE = GithubReleaseEnhancer; const C = GRE.config; const CN = C.CLASS_NAMES; return ` :root { --ghre-notes-max-height: ${GRE.store.state.releaseNotesMaxHeight || C.RELEASE_NOTES_MAX_HEIGHT_DEFAULT}px; } .${CN.FILTER_CONTAINER} { position: relative; display: inline-flex; align-items: center; margin-left: auto; } .${CN.PLATFORM_OPTIONS} { background-color: var(--overlay-bgColor); border: 0; border-radius: var(--borderRadius-large); box-shadow: var(--shadow-floating-small); position: absolute; right: 0; top: 100%; margin-top: 4px; z-index: 100; display: none; width: max-content; min-width: 280px; } .${CN.PLATFORM_OPTIONS_INNER} { max-height: 400px; overflow-y: auto; padding: 4px; display: flex; flex-direction: column; gap: 2px; } .${CN.POPUP_HEADER} { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; } .${CN.POPUP_SETTINGS_BTN} { background: none; border: none; cursor: pointer; color: var(--fgColor-muted); padding: 4px; line-height: 0; border-radius: 4px; } .${CN.POPUP_SETTINGS_BTN}:hover { background-color: var(--bgColor-muted); color: var(--fgColor-default); } .${CN.PLATFORM_OPTIONS_GRID} { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px 8px; } .${CN.FILTER_CONTAINER}.${CN.PLATFORM_OPTIONS_OPEN} .${CN.PLATFORM_OPTIONS} { display: block; } .${CN.OPTIONS_TITLE} { padding: 4px 8px; font-size: 12px; font-weight: bold; color: var(--fgColor-muted); text-transform: uppercase; display: block; pointer-events: none; grid-column: 1 / -1; } .ghre-action-buttons-container { grid-column: 1 / -1; display: flex; gap: 8px; padding: 2px 8px; } .${CN.ACTION_BUTTON} { flex: 1 1 0; padding: 5px; font-size: 12px; } ${C.SELECTORS.ASSETS_SUMMARY} { display: flex !important; align-items: center !important; width: 100%; } .${CN.SUMMARY_MARKER_ICON} { margin-right: 4px; transition: transform 0.15s ease-in-out; flex-shrink: 0; display: inline-flex; align-items: center; color: var(--fgColor-muted); } details[open] > summary .${CN.SUMMARY_MARKER_ICON} { transform: rotate(90deg); } .${CN.HIDDEN_ASSET_COUNTER} { margin-left: 4px; } .${CN.MARKDOWN_BODY}.${CN.RELEASE_NOTES_PANEL} { position: relative; max-height: var(--ghre-notes-max-height); overflow: hidden; transition: max-height 0.3s ease-in-out; padding-bottom: 40px; } .${CN.MARKDOWN_BODY}.${CN.RELEASE_NOTES_PANEL}.${CN.EXPANDED} { max-height: none; padding-bottom: 0; } .${CN.TOGGLE_BUTTON} { position: absolute; bottom: 0; left: 0; right: 0; width: 100%; height: 40px; box-sizing: border-box; padding: 10px; margin: 0; text-align: center; cursor: pointer; font-weight: var(--font-weight-bold, 600); color: var(--fgColor-accent); background: none; border: none; } .${CN.MARKDOWN_BODY}.${CN.RELEASE_NOTES_PANEL}:not(.${CN.EXPANDED}) .${CN.TOGGLE_BUTTON} { background: linear-gradient(to top, var(--bgColor-default), transparent); } .${CN.MARKDOWN_BODY}.${CN.RELEASE_NOTES_PANEL}.${CN.EXPANDED} .${CN.TOGGLE_BUTTON} { position: static; margin-top: 16px; height: auto; padding: 8px 0; border-top: 1px solid var(--borderColor-default); } .${CN.TOGGLE_BUTTON}:hover { text-decoration: underline; } .${CN.PLATFORM_OPTION}, .${CN.KEYWORD_TOGGLE_OPTION} { display: flex; align-items: center; font-size: 13px; color: var(--fgColor-default); white-space: nowrap; cursor: pointer; padding: 4px 8px; gap: 6px; transition: background-color 0.1s ease, color 0.1s ease; border-radius: 4px; } .${CN.PLATFORM_OPTION}:hover, .${CN.KEYWORD_TOGGLE_OPTION}:hover { background-color: var(--bgColor-accent-muted); } .${CN.PLATFORM_OPTION}.${CN.SELECTED}, .${CN.KEYWORD_TOGGLE_OPTION}.${CN.SELECTED} { background-color: var(--bgColor-accent-emphasis); color: var(--fgColor-onEmphasis); font-weight: var(--font-weight-bold, 600); } .${CN.PLATFORM_OPTION} svg, .${CN.KEYWORD_TOGGLE_OPTION} svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--fgColor-muted); } .${CN.PLATFORM_OPTION}.${CN.SELECTED} svg, .${CN.KEYWORD_TOGGLE_OPTION}.${CN.SELECTED} svg { color: var(--fgColor-onEmphasis); } .${CN.AVAILABILITY_DOT} { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; margin-left: auto; } .${CN.AVAILABILITY_DOT}[data-available="true"] { background-color: var(--bgColor-success-emphasis); } .${CN.AVAILABILITY_DOT}[data-available="false"] { background-color: var(--bgColor-danger-emphasis); } .${CN.PLATFORM_OPTION}[data-available="false"], .${CN.KEYWORD_TOGGLE_OPTION}[data-available="false"] { color: var(--fgColor-disabled); cursor: not-allowed; pointer-events: none; opacity: 0.7; } .${CN.PLATFORM_OPTION}[data-available="false"]:hover, .${CN.KEYWORD_TOGGLE_OPTION}[data-available="false"]:hover { background-color: transparent; } .${CN.OPTIONS_SEPARATOR} { border-top: 1px solid var(--borderColor-muted); margin: 4px 0; grid-column: 1 / -1; } .${CN.ARCH_TAG_CONTAINER} { display: flex; flex-wrap: wrap; justify-content: space-around; gap: 6px; padding: 4px 8px 2px 8px; grid-column: 1 / -1; } .${CN.ARCH_TAG} { flex-grow: 1; text-align: center; padding: 4px 8px; border: 1px solid var(--borderColor-muted); border-radius: 20px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; user-select: none; } .${CN.ARCH_TAG}:hover { border-color: var(--fgColor-accent); } .${CN.ARCH_TAG}.${CN.SELECTED} { background-color: var(--bgColor-accent-emphasis); color: var(--fgColor-onEmphasis); border-color: var(--bgColor-accent-emphasis); font-weight: 600; } .${CN.ARCH_TAG}[data-available="false"] { color: var(--fgColor-disabled); border-color: var(--borderColor-default); background-color: var(--bgColor-disabled); cursor: not-allowed; pointer-events: none; opacity: 0.6; } .${CN.HIDDEN_ASSET} { display: none !important; visibility: hidden !important; height: 0 !important; margin:0 !important; padding: 0 !important; border: none !important; opacity:0 !important; overflow: hidden !important; } .${CN.SETTINGS_OVERLAY} { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--overlay-backdrop-bgColor, rgba(0,0,0,0.5)); z-index: 2000; display: flex; align-items: center; justify-content: center; } .${CN.SETTINGS_PANEL} { position: relative; background-color: var(--overlay-bgColor); padding: 24px; border: 0; border-radius: 12px; box-shadow: var(--shadow-floating-large); min-width: 300px; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; } .${CN.SETTINGS_TITLE} { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--fgColor-default); } .${CN.SETTINGS_FORM_GROUP} { margin-bottom: 16px; } .${CN.SETTINGS_LABEL} { display: block; margin-bottom: 8px; font-weight: normal; color: var(--fgColor-muted); } .ghre-checkbox-group, .ghre-radio-group { display: flex; align-items: center; gap: 8px; } .ghre-radio-group-container { display: flex; flex-direction: row; align-items: center; gap: 16px; flex-wrap: wrap; } .ghre-pref-group { display: flex; align-items: center; gap: 4px; } .${CN.SETTINGS_INPUT}, .${CN.SETTINGS_TEXTAREA} { width: 100%; padding: 8px 12px; background-color: var(--bgColor-default); border-color: var(--borderColor-default); border-radius: 6px; border-style: solid; border-width: 1px; box-sizing: border-box; color: var(--fgColor-default); } .${CN.SETTINGS_TEXTAREA} { min-height: 60px; resize: vertical; } .${CN.SETTINGS_ACTIONS} { margin-top: 24px; display: flex; justify-content: flex-end; gap: 8px; } .${CN.SETTINGS_BUTTON} { } .${CN.RESET_CONFIRM_DIALOG} { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--bgColor-default); border: 1px solid var(--borderColor-danger); border-radius: 8px; box-shadow: var(--shadow-floating-large); padding: 16px; width: 90%; max-width: 400px; z-index: 2001; display: flex; flex-direction: column; gap: 12px; } .ghre-confirm-title { font-size: 16px; font-weight: 600; color: var(--fgColor-danger); } .ghre-confirm-message { font-size: 14px; color: var(--fgColor-default); } .ghre-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; } ${C.SELECTORS.ASSET_LIST_CONTAINER}.${CN.ASSET_LIST_LOADING} { opacity: 0; transition: opacity 0.2s ease-in-out !important; } ${C.SELECTORS.ASSET_LIST_CONTAINER} { opacity: 1; transition: opacity 0.2s ease-in-out !important; } @media (max-width: 768px) { .${CN.SETTINGS_PANEL} { width: 95%; padding: 16px; } } `; }, addStyleElement(id, css) { try { const attr = `data-ghre-style-${id}`; let styleElement = document.head.querySelector(`style[${attr}]`); if (!styleElement) { styleElement = GithubReleaseEnhancer.utils.createElement('style', { 'data-ghre-style-id': id, textContent: css }); document.head.appendChild(styleElement); } else if (styleElement.textContent !== css) { styleElement.textContent = css; } } catch (e) { console.error(`[GitHub Filter@${location.pathname}] Error adding styles:`, e); } }, addGlobalStyles() { this.addStyleElement('common', this.commonStylesTemplate()); }, updateMaxHeightVar(height) { document.documentElement.style.setProperty('--ghre-notes-max-height', `${height}px`); } }, theme: { watchSystemTheme() { const themeObserver = new MutationObserver(() => {}); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-color-mode', 'data-light-theme', 'data-dark-theme'] }); } }, }, // ================================================================================= // [模块] 核心逻辑 (Core) // ================================================================================= core: { releaseNotes: { initializeExpansion() { const GRE = GithubReleaseEnhancer; const { releaseNotesMaxHeight } = GRE.store.state; document.querySelectorAll(`.${GRE.config.CLASS_NAMES.MARKDOWN_BODY}.${GRE.config.CLASS_NAMES.RELEASE_NOTES_PANEL}`).forEach(p => { const b = p.querySelector(`.${GRE.config.CLASS_NAMES.TOGGLE_BUTTON}`); if (b) b.remove(); if (p.offsetParent !== null && p.scrollHeight > (releaseNotesMaxHeight || GRE.config.RELEASE_NOTES_MAX_HEIGHT_DEFAULT) ) { const tb = this.createToggleButton(); this.updateToggleButtonText(p, tb); tb.addEventListener('click', this.handleToggleClick.bind(this)); p.appendChild(tb); p.style.position = 'relative'; } else { p.classList.remove(GRE.config.CLASS_NAMES.EXPANDED); if (p.style.position === 'relative') p.style.position = ''; } }); }, handleToggleClick(e) { const b = e.currentTarget; const p = b.closest(`.${GithubReleaseEnhancer.config.CLASS_NAMES.MARKDOWN_BODY}.${GithubReleaseEnhancer.config.CLASS_NAMES.RELEASE_NOTES_PANEL}`); if (p) { p.classList.toggle(GithubReleaseEnhancer.config.CLASS_NAMES.EXPANDED); this.updateToggleButtonText(p, b); const isNowExpanded = p.classList.contains(GithubReleaseEnhancer.config.CLASS_NAMES.EXPANDED); if (isNowExpanded) { GithubReleaseEnhancer.notesCollapser.initForPanel(p); } else { GithubReleaseEnhancer.notesCollapser.cleanup(); const releaseContainer = p.closest('div.Box'); if(releaseContainer) { releaseContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } } }, createToggleButton() { return GithubReleaseEnhancer.utils.createElement('button', { type: 'button', className: GithubReleaseEnhancer.config.CLASS_NAMES.TOGGLE_BUTTON, 'aria-expanded': 'false' }); }, updateToggleButtonText(p, b) { const GRE = GithubReleaseEnhancer; const isExp = p.classList.contains(GRE.config.CLASS_NAMES.EXPANDED); b.textContent = isExp ? GRE.utils.getText('collapseButton') : GRE.utils.getText('expandButton'); b.setAttribute('aria-expanded', isExp.toString()); }, }, assetFilter: { getParsedAssets() { const GRE = GithubReleaseEnhancer; const cacheKey = window.location.pathname; if (GRE.assetCache.has(cacheKey)) { return GRE.assetCache.get(cacheKey); } try { const S = GRE.config.SELECTORS; const allAssetElements = Array.from(document.querySelectorAll(S.ASSET_ROW)); const parsedAssets = allAssetElements.map(assetElement => { const assetLink = assetElement.querySelector(S.ASSET_DOWNLOAD_LINK); let assetName; if (assetLink) { assetName = assetLink.textContent; } else { assetName = assetElement.querySelector('a')?.textContent || ''; } if (!assetName) return { element: assetElement, name: null, info: {} }; const assetInfo = GRE.utils.parseAssetInfo(assetName); return { element: assetElement, name: assetName, info: assetInfo }; }); GRE.assetCache.set(cacheKey, parsedAssets); return parsedAssets; } catch (error) { console.error(`[GitHub Filter@${location.pathname}] Error in getParsedAssets:`, error); return []; } }, isHiddenAsset(name, patterns) { const lowerName = name.replace(/\s+/g, ' ').trim().toLowerCase(); return patterns.some(pattern => { const lowerPattern = pattern.toLowerCase(); if (lowerPattern.startsWith('.')) { return lowerName.endsWith(lowerPattern); } return lowerName.includes(lowerPattern); }); }, preScanAssetsForAvailableFilters() { const GRE = GithubReleaseEnhancer; const parsedAssets = this.getParsedAssets(); const availableFilters = new Set(); const masterAvailableArchs = new Set(); let hasAssetsWithoutPlatform = false; parsedAssets.forEach(({ name: assetName, info: assetInfo }) => { if (!assetName) return; if (assetInfo.isSourceCode) { availableFilters.add('source'); } else if (this.isHiddenAsset(assetName, GRE.store.state.hiddenKeywords)) { availableFilters.add('keyword'); } else if (assetInfo.platform) { availableFilters.add(assetInfo.platform); } else { hasAssetsWithoutPlatform = true; } if (assetInfo.architecture) { masterAvailableArchs.add(assetInfo.architecture); } if (assetInfo.language) availableFilters.add('language'); if (assetInfo.resolution) availableFilters.add('resolution'); }); if (hasAssetsWithoutPlatform) { availableFilters.add('other_os'); } GRE.store.setState({ availableFilters, availableArchs: masterAvailableArchs, masterAvailableArchs }); }, updateAvailabilityMarkers(state) { const GRE = GithubReleaseEnhancer; const optionsContainer = state.filterUIInstance?.querySelector(`.${GRE.config.CLASS_NAMES.PLATFORM_OPTIONS_GRID}`); if (!optionsContainer) return; optionsContainer.querySelectorAll(`[data-platform-id], [data-filter-type]`).forEach(el => { const id = el.dataset.platformId || el.dataset.filterType; const isAvailable = state.availableFilters.has(id); el.setAttribute('data-available', isAvailable.toString()); let dot = el.querySelector(`.${GRE.config.CLASS_NAMES.AVAILABILITY_DOT}`); if (!dot) { dot = GRE.utils.createElement('span', { className: GRE.config.CLASS_NAMES.AVAILABILITY_DOT }); el.appendChild(dot); } dot.setAttribute('data-available', isAvailable.toString()); dot.title = isAvailable ? (state.isChinese ? "可用" : "Available") : (state.isChinese ? "不可用" : "Unavailable"); }); }, initialize() { const GRE = GithubReleaseEnhancer; this.ensureUIVisible(); const S = GRE.config.SELECTORS; const el = document.querySelector(`${S.ASSET_LIST_CONTAINER}, ${S.ASSETS_SUMMARY}`); if (el) { const assetList = document.querySelector(S.ASSET_LIST_CONTAINER); if (assetList) { assetList.classList.add(GRE.config.CLASS_NAMES.ASSET_LIST_LOADING); } this.handleAssetLoad(); } else { const o = new MutationObserver((_, obs) => { if (document.querySelector(`${S.ASSET_LIST_CONTAINER}, ${S.ASSETS_SUMMARY}`)) { this.handleAssetLoad(); obs.disconnect(); } }); o.observe(document.body, { childList: true, subtree: true }); } }, handleAssetLoad() { const GRE = GithubReleaseEnhancer; GRE.assetCache.delete(window.location.pathname); this.preScanAssetsForAvailableFilters(); GRE.actions.initializeFilterStates(); }, ensureUIVisible() { const GRE = GithubReleaseEnhancer; if (GRE.store.state.isMainReleasesPage) return; if (!GRE.store.state.filterUIInstance || !document.body.contains(GRE.store.state.filterUIInstance)) { GRE.store.setState({ filterUIInstance: this.createUI() }); } }, summaryCaptureListener: function(event) { const GRE = GithubReleaseEnhancer; if (event.target.closest(`.${GRE.config.CLASS_NAMES.FILTER_CONTAINER}`)) { event.preventDefault(); } }, closePopup() { const GRE = GithubReleaseEnhancer; if (!GRE.store.state.isDropdownOpen) return; const wrapper = GRE.store.state.filterUIInstance; if (wrapper) { wrapper.classList.remove(GRE.config.CLASS_NAMES.PLATFORM_OPTIONS_OPEN); const filterButton = wrapper.querySelector(`.${GRE.config.CLASS_NAMES.FILTER_BUTTON}`); if (filterButton) { filterButton.setAttribute('aria-expanded', 'false'); filterButton.focus(); } } GRE.store.setState({ isDropdownOpen: false }); }, handleGlobalClick(event) { const GRE = GithubReleaseEnhancer; if (GRE.store.state.isDropdownOpen && GRE.store.state.filterUIInstance && !GRE.store.state.filterUIInstance.contains(event.target)) { this.closePopup(); } }, createUI() { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN, SELECTORS: S } = GRE.config; const { icons } = GRE; const { createElement } = GRE.utils; try { let assetsSummary = document.querySelector(S.ASSETS_SUMMARY); if (!assetsSummary) return null; assetsSummary.querySelector(`.${CN.SUMMARY_MARKER_ICON}`)?.remove(); assetsSummary.querySelector(`.${CN.FILTER_CONTAINER}`)?.remove(); Object.assign(assetsSummary.style, { display: 'flex', alignItems: 'center' }); const newMarkerSvgSpan = createElement('span', { className: CN.SUMMARY_MARKER_ICON, innerHTML: icons.disclosure_triangle_right_svg }); assetsSummary.insertBefore(newMarkerSvgSpan, assetsSummary.firstChild); const filterInteractionWrapper = createElement('div', { className: CN.FILTER_CONTAINER, style: { marginLeft: 'auto', display: 'inline-flex' } }); filterInteractionWrapper.appendChild(this.createFilterButton()); filterInteractionWrapper.appendChild(this.createPlatformOptions()); assetsSummary.appendChild(filterInteractionWrapper); let hiddenCounterElement = assetsSummary.querySelector(S.HIDDEN_ASSET_COUNTER); if (!hiddenCounterElement) { hiddenCounterElement = createElement('span', { className: `Counter ml-1 ${CN.HIDDEN_ASSET_COUNTER}`, title: GRE.utils.getText('hiddenAssetCountTitle'), style: { display: 'none' } }); assetsSummary.insertBefore(hiddenCounterElement, filterInteractionWrapper); } assetsSummary.removeEventListener('click', GRE.core.assetFilter.summaryCaptureListener, true); assetsSummary.addEventListener('click', GRE.core.assetFilter.summaryCaptureListener, true); return filterInteractionWrapper; } catch (e) { console.error(`[GitHub Filter@${location.pathname}] Error creating filter UI:`, e); return null; } }, createFilterButton() { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN, SELECTORS: S } = GRE.config; const { icons } = GRE; const btn = GRE.utils.createElement('button', { type: 'button', className: `btn btn-sm hx_rsm-trigger ${CN.FILTER_BUTTON}`, style: { paddingLeft: '0.75rem', paddingRight: '0.75rem', marginRight: '0px', display: 'inline-flex', alignItems: 'center' }, 'aria-haspopup': 'true', 'aria-expanded': 'false', innerHTML: `${icons.filter}<span>${GRE.utils.getText('filterButton')}</span><span ${S.FILTER_COUNTER.slice(1, -1)} title="${GRE.utils.getText('activeFilterCountTitle')}" class="Counter ml-2">0</span>` }); btn.addEventListener('click', (event) => { event.stopPropagation(); GRE.actions.toggleDropdown(); }); return btn; }, updateFilterCounter(state) { if (!state.filterUIInstance) return; const counterElement = state.filterUIInstance.querySelector(GithubReleaseEnhancer.config.SELECTORS.FILTER_COUNTER); if (counterElement) { let count = 0; state.selectedPlatforms.forEach(p => { if(state.availableFilters.has(p)) count++; }); state.selectedArchs.forEach(a => { if (state.masterAvailableArchs.has(a)) count++; }); if (state.filterMatchLanguage && state.availableFilters.has('language')) count++; if (state.filterMatchResolution && state.availableFilters.has('resolution')) count++; if (state.hideByKeyword && state.availableFilters.has('keyword')) count++; if (state.hideSourceCode && state.availableFilters.has('source')) count++; counterElement.textContent = count; } }, updateHiddenAssetsCounter(hiddenCount = 0) { const counterEl = document.querySelector(GithubReleaseEnhancer.config.SELECTORS.HIDDEN_ASSET_COUNTER); if (counterEl) { if (hiddenCount > 0) { counterEl.textContent = `(${hiddenCount} ${GithubReleaseEnhancer.store.state.isChinese ? '隐藏' : 'hidden'})`; counterEl.style.display = ''; } else { counterEl.style.display = 'none'; } } }, _updateOptionVisuals(element, isSelected, isAvailable) { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN } = GRE.config; const { icons } = GRE; element.setAttribute('data-available', isAvailable.toString()); const shouldBeSelected = isAvailable ? isSelected : false; element.classList.toggle(CN.SELECTED, shouldBeSelected); element.setAttribute('aria-checked', shouldBeSelected.toString()); const iconSpan = element.querySelector('span:first-child'); if (iconSpan) { iconSpan.innerHTML = shouldBeSelected ? icons.checkbox_checked : icons.checkbox_unchecked; } }, createSpecificFilterToggle(type, stateKey, initialLabel) { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN } = GRE.config; const { createElement } = GRE.utils; const el = createElement('div', { className: CN.KEYWORD_TOGGLE_OPTION, role: 'menuitemcheckbox', dataset: { filterType: type }, tabindex: '-1' }); el.appendChild(createElement('span')); el.appendChild(createElement('span', { textContent: initialLabel })); this.updateSpecificFilterVisualState(type, GRE.store.state[stateKey], el); return el; }, updateSpecificFilterVisualState(type, isActive, elementOrContainer = document) { const GRE = GithubReleaseEnhancer; if (!elementOrContainer && GRE.store.state.filterUIInstance) { elementOrContainer = GRE.store.state.filterUIInstance; } else if (!elementOrContainer) { return; } const selector = `[data-filter-type="${type}"]`; let el = elementOrContainer.matches?.(selector) ? elementOrContainer : elementOrContainer.querySelector?.(selector); if (el) { const isAvailable = GRE.store.state.availableFilters.has(type); this._updateOptionVisuals(el, isActive, isAvailable); } }, createPlatformOptions() { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN } = GRE.config; const { createElement } = GRE.utils; const state = GRE.store.state; const optsCont = createElement('div', { className: `${CN.PLATFORM_OPTIONS}` }); if (!state.isTouchDevice) { let popupCloseTimeout; const clearCloseTimeout = () => clearTimeout(popupCloseTimeout); optsCont.addEventListener('mouseleave', () => { clearCloseTimeout(); popupCloseTimeout = setTimeout(() => this.closePopup(), GRE.config.POPUP_LEAVE_CLOSE_DELAY); }); optsCont.addEventListener('mouseenter', clearCloseTimeout); } const innerCont = createElement('div', { className: CN.PLATFORM_OPTIONS_INNER }); const header = createElement('div', { className: CN.POPUP_HEADER }); const title = createElement('div', { className: CN.OPTIONS_TITLE, style: { padding: 0, gridColumn: 'auto' }, textContent: GRE.utils.getText('platformFilterTitle') }); const settingsBtn = createElement('button', { className: CN.POPUP_SETTINGS_BTN, title: GRE.utils.getText('settingsMenuName'), innerHTML: GRE.icons.gear }); settingsBtn.addEventListener('click', () => { this.closePopup(); GRE.settings.showSettingsPanel(); }); header.appendChild(title); header.appendChild(settingsBtn); innerCont.appendChild(header); const gridCont = createElement('div', { className: CN.PLATFORM_OPTIONS_GRID }); GRE.config.PLATFORMS.forEach(p => gridCont.appendChild(this.createPlatformOptionElement(p))); gridCont.appendChild(createElement('div')); gridCont.appendChild(this.createArchTagContainer()); gridCont.appendChild(createElement('div', { className: CN.OPTIONS_SEPARATOR })); gridCont.appendChild(createElement('div', { className: CN.OPTIONS_TITLE, textContent: GRE.utils.getText('supplementaryFilterTitle') })); gridCont.appendChild(this.createSpecificFilterToggle('language', 'filterMatchLanguage', `${GRE.utils.getText('langLabel')} (${GRE.config.LANGUAGES[state.currentUserLanguage]?.name || state.currentUserLanguage})`)); const resText = state.currentUserResolutionCategory === 'hd' ? 'HD' : (GRE.config.RESOLUTIONS['sd']?.name || 'SD'); gridCont.appendChild(this.createSpecificFilterToggle('resolution', 'filterMatchResolution', `${GRE.utils.getText('resLabel')} (${resText})`)); gridCont.appendChild(this.createSpecificFilterToggle('source', 'hideSourceCode', GRE.utils.getText('sourceCodeLabel'))); gridCont.appendChild(this.createSpecificFilterToggle('keyword', 'hideByKeyword', GRE.utils.getText('keywordLabel'))); const actionsContainer = createElement('div', { className: 'ghre-action-buttons-container' }); const selectAllBtn = createElement('button', { textContent: GRE.utils.getText('selectAllLabel'), className: `btn btn-sm ${CN.ACTION_BUTTON} ghre-select-all-btn` }); const deselectAllBtn = createElement('button', { textContent: GRE.utils.getText('deselectAllLabel'), className: `btn btn-sm ${CN.ACTION_BUTTON} ghre-deselect-all-btn` }); const restoreBtn = createElement('button', { textContent: GRE.utils.getText('restoreLabel'), className: `btn btn-sm btn-primary ${CN.ACTION_BUTTON} ghre-restore-btn`, style: { display: 'none', width: '100%' } }); selectAllBtn.addEventListener('click', () => this.toggleAllFilters(true)); deselectAllBtn.addEventListener('click', () => this.toggleAllFilters(false)); restoreBtn.addEventListener('click', () => GithubReleaseEnhancer.actions.restoreInitialFilters()); actionsContainer.appendChild(selectAllBtn); actionsContainer.appendChild(deselectAllBtn); actionsContainer.appendChild(restoreBtn); gridCont.appendChild(actionsContainer); innerCont.appendChild(gridCont); optsCont.appendChild(innerCont); optsCont.addEventListener('click', (e) => { const interactiveTarget = e.target.closest(`.${CN.PLATFORM_OPTION}, .${CN.KEYWORD_TOGGLE_OPTION}, .${CN.ARCH_TAG}`); if (interactiveTarget) { e.stopPropagation(); this.handleMenuClick(e, interactiveTarget); } else if (!e.target.closest(`.${CN.ACTION_BUTTON}, .${CN.POPUP_SETTINGS_BTN}`)) { e.stopPropagation(); } }); return optsCont; }, createArchTagContainer() { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN, ARCH_TAGS_CONFIG } = GRE.config; const { createElement } = GRE.utils; const container = createElement('div', { className: CN.ARCH_TAG_CONTAINER }); ARCH_TAGS_CONFIG.forEach(arch => { const tagEl = createElement('div', { className: CN.ARCH_TAG, textContent: arch.name, dataset: { archId: arch.id } }); container.appendChild(tagEl); }); return container; }, toggleAllFilters(select) { GithubReleaseEnhancer.actions.updateAndSaveAllFilters(select); }, updateActionButtons(state) { const container = state.filterUIInstance; if (!container) return; const actionButtonsContainer = container.querySelector('.ghre-action-buttons-container'); if (!actionButtonsContainer) return; const selectAllBtn = actionButtonsContainer.querySelector('.ghre-select-all-btn'); const deselectAllBtn = actionButtonsContainer.querySelector('.ghre-deselect-all-btn'); const restoreBtn = actionButtonsContainer.querySelector('.ghre-restore-btn'); if (selectAllBtn && deselectAllBtn && restoreBtn) { const currentFilters = { selectedPlatforms: state.selectedPlatforms, selectedArchs: state.selectedArchs, filterMatchLanguage: state.filterMatchLanguage, filterMatchResolution: state.filterMatchResolution, hideByKeyword: state.hideByKeyword, hideSourceCode: state.hideSourceCode, }; const isDifferentFromInitial = !this.areFiltersEqual(currentFilters, state.initialFilterState); selectAllBtn.style.display = isDifferentFromInitial ? 'none' : ''; deselectAllBtn.style.display = isDifferentFromInitial ? 'none' : ''; restoreBtn.style.display = isDifferentFromInitial ? '' : 'none'; } }, areFiltersEqual(filtersA, filtersB) { if (!filtersA || !filtersB) return false; const keys = ['filterMatchLanguage', 'filterMatchResolution', 'hideByKeyword', 'hideSourceCode']; for (const key of keys) { if (filtersA[key] !== filtersB[key]) return false; } const setKeys = ['selectedPlatforms', 'selectedArchs']; for (const key of setKeys) { const setA = filtersA[key]; const setB = filtersB[key]; if (setA.size !== setB.size) return false; for (const item of setA) { if (!setB.has(item)) return false; } } return true; }, handleMenuClick(event, target) { const GRE = GithubReleaseEnhancer; const { platformId, filterType, archId } = target.dataset; if (platformId) { GRE.actions.togglePlatform(platformId); } else if (filterType) { GRE.actions.toggleSupplementaryFilter(filterType); } else if (archId) { GRE.actions.toggleArch(archId); } }, createPlatformOptionElement(platform) { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN } = GRE.config; const { createElement } = GRE.utils; const el = createElement('div', { className: CN.PLATFORM_OPTION, role: 'menuitemcheckbox', dataset: { platformId: platform.id }, tabindex: '-1' }); el.appendChild(createElement('span')); el.appendChild(createElement('span', { textContent: platform.name })); this.updatePlatformOptionVisualState(el, platform.id, GRE.store.state.selectedPlatforms.has(platform.id)); return el; }, updatePlatformOptionVisualState(el, pId, isSel) { const isAvailable = GithubReleaseEnhancer.store.state.availableFilters.has(pId); this._updateOptionVisuals(el, isSel, isAvailable); }, updateAllPlatformOptionsVisualState(state) { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN } = GRE.config; const container = state.filterUIInstance; if (!container) return; GRE.config.PLATFORMS.forEach(p => { const opt = container.querySelector(`.${CN.PLATFORM_OPTION}[data-platform-id="${p.id}"]`); if (opt) { this.updatePlatformOptionVisualState(opt, p.id, state.selectedPlatforms.has(p.id)); } }); }, updateAllArchTagsVisualState(state) { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN, ARCH_TAGS_CONFIG } = GRE.config; const container = state.filterUIInstance; if (!container) return; const parsedAssets = this.getParsedAssets(); const availableArchsNow = new Set(); const assetsToConsider = state.selectedPlatforms.size > 0 ? parsedAssets.filter(asset => asset.info.platform && state.selectedPlatforms.has(asset.info.platform)) : parsedAssets; assetsToConsider.forEach(asset => { if (asset.info.architecture) { availableArchsNow.add(asset.info.architecture); } }); ARCH_TAGS_CONFIG.forEach(arch => { const tagEl = container.querySelector(`.${CN.ARCH_TAG}[data-arch-id="${arch.id}"]`); if (tagEl) { const isAvailable = availableArchsNow.has(arch.id); const isSelected = state.selectedArchs.has(arch.id); tagEl.setAttribute('data-available', isAvailable.toString()); tagEl.classList.toggle(CN.SELECTED, isSelected); } }); const setsAreEqual = (setA, setB) => setA.size === setB.size && [...setA].every(value => setB.has(value)); if (!setsAreEqual(state.availableArchs, availableArchsNow)) { GRE.store.setState({ availableArchs: availableArchsNow }); } }, rules: { platform(assetInfo, state) { const { selectedPlatforms } = state; if (selectedPlatforms.size === 0) return true; if (assetInfo.platform) return selectedPlatforms.has(assetInfo.platform); return selectedPlatforms.has('other_os'); }, arch(assetInfo, state) { const { selectedArchs } = state; if (selectedArchs.size === 0) return true; if (assetInfo.architecture === null) return true; return selectedArchs.has(assetInfo.architecture); }, language(assetInfo, state, context) { if (!state.filterMatchLanguage || !context?.hasInfo.language) return true; if (assetInfo.language === null) return !context.hasMatch.language; return assetInfo.language === state.currentUserLanguage; }, resolution(assetInfo, state, context) { if (!state.filterMatchResolution || !context?.hasInfo.resolution) return true; if (assetInfo.resolution === null) return !context.hasMatch.resolution; return assetInfo.resolution === state.currentUserResolutionCategory; }, }, filterAssets(state) { const GRE = GithubReleaseEnhancer; if (state.isMainReleasesPage) return; const parsedAssets = this.getParsedAssets(); if (parsedAssets.length === 0) { this.updateHiddenAssetsCounter(0); return; } const platformScanResults = parsedAssets.reduce((acc, { info }) => { const platformKey = info.platform || 'unknown'; if (!acc[platformKey]) { acc[platformKey] = { hasInfo: { language: false, resolution: false }, hasMatch: { language: false, resolution: false } }; } if (info.language) { acc[platformKey].hasInfo.language = true; if (info.language === state.currentUserLanguage) acc[platformKey].hasMatch.language = true; } if (info.resolution) { acc[platformKey].hasInfo.resolution = true; if (info.resolution === state.currentUserResolutionCategory) acc[platformKey].hasMatch.resolution = true; } return acc; }, {}); let hiddenCount = 0; const domUpdateTasks = []; parsedAssets.forEach(asset => { const { element: assetElement, name: assetName, info: assetInfo } = asset; if (!assetName) { domUpdateTasks.push({ element: assetElement, show: true }); return; } let shouldShow; if (assetInfo.isSourceCode) { shouldShow = !state.hideSourceCode; } else if (assetInfo.isByKeyword) { shouldShow = !state.hideByKeyword; } else { const context = platformScanResults[assetInfo.platform || 'unknown']; const passesPlatform = this.rules.platform(assetInfo, state) && this.rules.arch(assetInfo, state); const passesSupplementary = this.rules.language(assetInfo, state, context) && this.rules.resolution(assetInfo, state, context); shouldShow = passesPlatform && passesSupplementary; } domUpdateTasks.push({ element: assetElement, show: shouldShow }); if (!shouldShow) { hiddenCount++; } }); const assetList = document.querySelector(GRE.config.SELECTORS.ASSET_LIST_CONTAINER); requestAnimationFrame(() => { domUpdateTasks.forEach(task => { task.element.classList.toggle(GRE.config.CLASS_NAMES.HIDDEN_ASSET, !task.show); }); if (assetList) { assetList.classList.remove(GRE.config.CLASS_NAMES.ASSET_LIST_LOADING); } }); this.updateHiddenAssetsCounter(hiddenCount); } } }, // ================================================================================= // [模块] 页面与导航 (Page & Navigation) // ================================================================================= page: { observer: { start() { try { const GRE = GithubReleaseEnhancer; const mainContent = document.querySelector(GRE.config.SELECTORS.MAIN_CONTENT) || document.body; let domChangeDebounceTimer; const callback = (mutationsList) => { clearTimeout(domChangeDebounceTimer); domChangeDebounceTimer = setTimeout(() => this.handleChanges(mutationsList), GRE.config.DEBOUNCE_DELAY); }; const observer = new MutationObserver(callback); observer.observe(mainContent, { childList: true, subtree: true }); GRE._observer = observer; } catch (e) { console.error(`[GitHub Filter@${location.pathname}] Error starting DOM observer:`, e); } }, handleChanges(mutations) { const GRE = GithubReleaseEnhancer; const S = GRE.config.SELECTORS; let needsReleaseNotesUpdate = false; let needsAssetFilterUpdate = false; const hasElementNodes = mutations.some(m => m.addedNodes.length > 0 && Array.from(m.addedNodes).some(n => n.nodeType === Node.ELEMENT_NODE)); if (!hasElementNodes) return; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches(`.${GRE.config.CLASS_NAMES.MARKDOWN_BODY}.${GRE.config.CLASS_NAMES.RELEASE_NOTES_PANEL}`) || node.querySelector(`.${GRE.config.CLASS_NAMES.MARKDOWN_BODY}.${GRE.config.CLASS_NAMES.RELEASE_NOTES_PANEL}`)) { needsReleaseNotesUpdate = true; } const assetSummarySelector = `${S.ASSET_LIST_CONTAINER}, ${S.ASSETS_SUMMARY}`; if (!GRE.store.state.isMainReleasesPage && (node.matches(assetSummarySelector) || node.querySelector(assetSummarySelector))) { needsAssetFilterUpdate = true; } } } } if (needsReleaseNotesUpdate) { requestAnimationFrame(() => GRE.core.releaseNotes.initializeExpansion()); } if (needsAssetFilterUpdate) { requestAnimationFrame(() => GRE.core.assetFilter.handleAssetLoad()); } } }, navigation: { setupListeners() { try { if (navigationReInit) { document.removeEventListener('turbo:load', navigationReInit); window.removeEventListener('popstate', navigationReInit); } navigationReInit = () => setTimeout(() => GithubReleaseEnhancer.init(), 100); document.addEventListener('turbo:load', navigationReInit); window.addEventListener('popstate', navigationReInit); } catch (e) { console.error(`[GitHub Filter@${location.pathname}] Error setting up navigation listeners:`, e); } } } }, // ================================================================================= // [模块] 设置 (Settings) // ================================================================================= settings: { parseHiddenKeywords(input) { if (!input || typeof input !== 'string') return []; const patterns = []; const regex = /'([^']*)'|([^,]+)/g; let match; while ((match = regex.exec(input)) !== null) { const pattern = (match[1] !== undefined ? match[1] : match[2]).trim(); if (pattern) { patterns.push(pattern); } } return patterns; }, loadUserSettings() { const GRE = GithubReleaseEnhancer; const C = GRE.config; const defaults = { releaseNotesMaxHeight: C.RELEASE_NOTES_MAX_HEIGHT_DEFAULT, hiddenKeywords: [...C.HIDDEN_KEYWORDS_DEFAULT], clickOutsideToCollapse: true, filterMode: 'smart', preferredFilters: {}, }; const storedSettings = GM_getValue(C.STORAGE_KEY_SETTINGS, {}); if (storedSettings.preferredFilters) { if (Array.isArray(storedSettings.preferredFilters.selectedPlatforms)) { storedSettings.preferredFilters.selectedPlatforms = new Set(storedSettings.preferredFilters.selectedPlatforms); } if (Array.isArray(storedSettings.preferredFilters.selectedArchs)) { storedSettings.preferredFilters.selectedArchs = new Set(storedSettings.preferredFilters.selectedArchs); } } const loadedSettings = { ...defaults, ...storedSettings }; GRE.store.setState(loadedSettings); }, saveUserSettings(settingsToSave) { const GRE = GithubReleaseEnhancer; const C = GRE.config; GRE.store.setState(settingsToSave); const currentSettings = GM_getValue(C.STORAGE_KEY_SETTINGS, {}); let processedSettings = {...settingsToSave}; if (processedSettings.preferredFilters) { const prefsToSave = { ...processedSettings.preferredFilters }; if (prefsToSave.selectedPlatforms instanceof Set) { prefsToSave.selectedPlatforms = Array.from(prefsToSave.selectedPlatforms); } if (prefsToSave.selectedArchs instanceof Set) { prefsToSave.selectedArchs = Array.from(prefsToSave.selectedArchs); } processedSettings.preferredFilters = prefsToSave; } const newSettings = { ...currentSettings, ...processedSettings }; GM_setValue(C.STORAGE_KEY_SETTINGS, newSettings); console.log('[GitHub Filter] Settings saved.', newSettings); }, resetAllSettings() { const GRE = GithubReleaseEnhancer; GM_deleteValue(GRE.config.STORAGE_KEY_SETTINGS); location.reload(); }, showResetConfirmation(overlay) { const GRE = GithubReleaseEnhancer; const { createElement, getText } = GRE.utils; const { CLASS_NAMES: CN } = GRE.config; overlay.querySelector(`.${CN.RESET_CONFIRM_DIALOG}`)?.remove(); const dialog = createElement('div', { className: CN.RESET_CONFIRM_DIALOG }); dialog.innerHTML = ` <div class="ghre-confirm-title">${getText('resetConfirmationTitle')}</div> <div class="ghre-confirm-message">${getText('resetConfirmationMessage')}</div> <div class="ghre-confirm-actions"> <button class="btn btn-sm" data-action="cancel-reset">${getText('settingsCancelButton')}</button> <button class="btn btn-sm btn-danger" data-action="confirm-reset">${getText('resetConfirmButton')}</button> </div> `; dialog.addEventListener('click', (e) => { e.stopPropagation(); const action = e.target.dataset.action; if (action === 'cancel-reset') { dialog.remove(); } else if (action === 'confirm-reset') { this.resetAllSettings(); } }); overlay.appendChild(dialog); }, createSettingsPanel() { try { const GRE = GithubReleaseEnhancer; const { CLASS_NAMES: CN, SELECTORS: S } = GRE.config; const { createElement } = GRE.utils; const state = GRE.store.state; if (document.querySelector(`.${CN.SETTINGS_OVERLAY}`)) { return; } const settingsOverlayElement = createElement('div', { className: CN.SETTINGS_OVERLAY, style: { display: 'none' } }); const settingsPanelElement = createElement('div', { className: CN.SETTINGS_PANEL, role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'ghre-settings-title-id' }); const createCheckboxGroup = (id, labelKey, checked) => { return `<div class="ghre-checkbox-group ${CN.SETTINGS_FORM_GROUP}"><input type="checkbox" id="${id}"${checked ? ' checked' : ''}><label for="${id}" class="${CN.SETTINGS_LABEL}" style="margin-bottom:0;">${GRE.utils.getText(labelKey)}</label></div>`; }; const createRadioGroup = (name, value, labelKey, checked) => { const id = `ghre-radio-${value}`; return `<div class="ghre-radio-group"><input type="radio" id="${id}" name="${name}" value="${value}"${checked ? ' checked' : ''}><label for="${id}" class="${CN.SETTINGS_LABEL}" style="margin-bottom:0;">${GRE.utils.getText(labelKey)}</label></div>`; }; let panelHTML = `<div id="ghre-settings-title-id" class="${CN.SETTINGS_TITLE}">${GRE.utils.getText('settingsPanelTitle')}</div>`; panelHTML += `<div class="${CN.SETTINGS_FORM_GROUP}"> <label class="${CN.SETTINGS_LABEL}">${GRE.utils.getText('filterModeLabel')}</label> <div class="ghre-radio-group-container"> ${createRadioGroup('filterMode', 'smart', 'intelligentFilterLabel', state.filterMode === 'smart')} <div class="ghre-pref-group"> ${createRadioGroup('filterMode', 'preferred', 'preferredFilterLabel', state.filterMode === 'preferred')} <button class="btn btn-sm" id="ghreSavePrefsBtn" style="margin-left: 4px;">${GRE.utils.getText('savePrefsButton')}</button> </div> </div> </div>`; panelHTML += `<div class="${CN.SETTINGS_FORM_GROUP}"><label for="${S.SETTINGS_MAX_HEIGHT_INPUT.substring(1)}" class="${CN.SETTINGS_LABEL}">${GRE.utils.getText('settingsMaxHeightLabel')}</label><input type="number" id="${S.SETTINGS_MAX_HEIGHT_INPUT.substring(1)}" class="${CN.SETTINGS_INPUT}" value="${state.releaseNotesMaxHeight}" min="50" step="10"></div>`; panelHTML += `<div class="${CN.SETTINGS_FORM_GROUP}"><label for="${S.SETTINGS_HIDDEN_RULES_TEXTAREA.substring(1)}" class="${CN.SETTINGS_LABEL}">${GRE.utils.getText('settingsHiddenRuleLabel')}</label><textarea id="${S.SETTINGS_HIDDEN_RULES_TEXTAREA.substring(1)}" class="${CN.SETTINGS_TEXTAREA}" placeholder="${GRE.utils.getText('settingsHiddenRulePlaceholder')}">${state.hiddenKeywords.map(k => k.includes(' ') ? `'${k}'` : k).join(', ')}</textarea></div>`; panelHTML += createCheckboxGroup(S.SETTINGS_CLICK_OUTSIDE_CHECKBOX.substring(1), 'settingsClickOutsideLabel', state.clickOutsideToCollapse); const actionsContainer = createElement('div', { className: CN.SETTINGS_ACTIONS }); const resetButton = createElement('button', { className: `btn btn-danger ${CN.SETTINGS_BUTTON}`, dataset: { action: 'reset' }, textContent: GRE.utils.getText('settingsResetButton') }); const spacer = createElement('div', { style: { flexGrow: '1' } }); const cancelButton = createElement('button', { className: `btn ${CN.SETTINGS_BUTTON}`, dataset: { action: 'cancel' }, textContent: GRE.utils.getText('settingsCancelButton') }); const saveButton = createElement('button', { className: `btn btn-primary ${CN.SETTINGS_BUTTON}`, dataset: { action: 'save' }, textContent: GRE.utils.getText('settingsSaveButton') }); actionsContainer.append(resetButton, spacer, cancelButton, saveButton); settingsPanelElement.innerHTML = panelHTML; settingsPanelElement.appendChild(actionsContainer); settingsOverlayElement.appendChild(settingsPanelElement); document.body.appendChild(settingsOverlayElement); const savePrefsBtn = settingsPanelElement.querySelector('#ghreSavePrefsBtn'); const preferredRadio = settingsPanelElement.querySelector('input[name="filterMode"][value="preferred"]'); const smartRadio = settingsPanelElement.querySelector('input[name="filterMode"][value="smart"]'); const updateSavePrefsBtnState = () => { savePrefsBtn.disabled = !preferredRadio.checked; }; preferredRadio.addEventListener('change', updateSavePrefsBtnState); smartRadio.addEventListener('change', updateSavePrefsBtnState); updateSavePrefsBtnState(); savePrefsBtn.addEventListener('click', (e) => { e.preventDefault(); const currentFilters = { selectedPlatforms: GRE.store.state.selectedPlatforms, selectedArchs: GRE.store.state.selectedArchs, filterMatchLanguage: GRE.store.state.filterMatchLanguage, filterMatchResolution: GRE.store.state.filterMatchResolution, hideByKeyword: GRE.store.state.hideByKeyword, hideSourceCode: GRE.store.state.hideSourceCode, }; this.saveUserSettings({ preferredFilters: currentFilters }); const btn = e.target; const originalText = btn.textContent; btn.textContent = GRE.utils.getText('prefsSavedAlert'); btn.disabled = true; setTimeout(() => { btn.textContent = originalText; updateSavePrefsBtnState(); }, 2000); }); settingsPanelElement.querySelector(S.SETTINGS_RESET_BUTTON).addEventListener('click', (e) => { e.preventDefault(); this.showResetConfirmation(settingsOverlayElement); }); settingsPanelElement.querySelector(S.SETTINGS_SAVE_BUTTON).addEventListener('click', (e) => { e.preventDefault(); const maxHeight = parseInt(settingsPanelElement.querySelector(S.SETTINGS_MAX_HEIGHT_INPUT).value, 10); const hiddenRulesInput = settingsPanelElement.querySelector(S.SETTINGS_HIDDEN_RULES_TEXTAREA).value; const hiddenKeywords = this.parseHiddenKeywords(hiddenRulesInput); const clickOutside = settingsPanelElement.querySelector(S.SETTINGS_CLICK_OUTSIDE_CHECKBOX).checked; const filterMode = settingsPanelElement.querySelector('input[name="filterMode"]:checked').value; this.saveUserSettings({ releaseNotesMaxHeight: (!isNaN(maxHeight) && maxHeight >= 50) ? maxHeight : GRE.config.RELEASE_NOTES_MAX_HEIGHT_DEFAULT, hiddenKeywords: hiddenKeywords.length > 0 ? hiddenKeywords : [...GRE.config.HIDDEN_KEYWORDS_DEFAULT], clickOutsideToCollapse: clickOutside, filterMode: filterMode, }); this.hideSettingsPanel(); GRE.actions.initializeFilterStates(); }); settingsPanelElement.querySelector(S.SETTINGS_CANCEL_BUTTON).addEventListener('click', (e) => { e.preventDefault(); this.hideSettingsPanel(); }); settingsOverlayElement.addEventListener('click', (e) => { if (e.target === settingsOverlayElement) { if (!settingsOverlayElement.querySelector(`.${CN.RESET_CONFIRM_DIALOG}`)) { this.hideSettingsPanel(); } } }); GRE.store.setState({ settingsOverlayElement }); } catch (error) { console.error(`[GitHub Filter@${location.pathname}] Failed to create settings panel:`, error); } }, showSettingsPanel() { const GRE = GithubReleaseEnhancer; const S = GRE.config.SELECTORS; let { settingsOverlayElement } = GRE.store.state; if (!settingsOverlayElement || !document.body.contains(settingsOverlayElement)) { this.createSettingsPanel(); settingsOverlayElement = GRE.store.state.settingsOverlayElement; } if (settingsOverlayElement) { const panel = settingsOverlayElement.querySelector(`.${GRE.config.CLASS_NAMES.SETTINGS_PANEL}`); panel.querySelector(`input[name="filterMode"][value="${GRE.store.state.filterMode}"]`).checked = true; panel.querySelector('#ghreSavePrefsBtn').disabled = GRE.store.state.filterMode !== 'preferred'; panel.querySelector(S.SETTINGS_MAX_HEIGHT_INPUT).value = GRE.store.state.releaseNotesMaxHeight; panel.querySelector(S.SETTINGS_HIDDEN_RULES_TEXTAREA).value = GRE.store.state.hiddenKeywords.map(k => k.includes(' ') ? `'${k}'` : k).join(', '); panel.querySelector(S.SETTINGS_CLICK_OUTSIDE_CHECKBOX).checked = GRE.store.state.clickOutsideToCollapse; settingsOverlayElement.style.display = 'flex'; } }, hideSettingsPanel() { const { settingsOverlayElement } = GithubReleaseEnhancer.store.state; if (settingsOverlayElement) { settingsOverlayElement.style.display = 'none'; settingsOverlayElement.querySelector(`.${GithubReleaseEnhancer.config.CLASS_NAMES.RESET_CONFIRM_DIALOG}`)?.remove(); } }, registerMenu() { const menuText = GithubReleaseEnhancer.utils.getText('settingsMenuName'); GM_registerMenuCommand(menuText, () => GithubReleaseEnhancer.settings.showSettingsPanel()); } }, // ================================================================================= // [模块] 用户操作 (Actions) // ================================================================================= actions: { togglePlatform(platformId) { const { state, setState } = GithubReleaseEnhancer.store; const newSelectedPlatforms = new Set(state.selectedPlatforms); newSelectedPlatforms.has(platformId) ? newSelectedPlatforms.delete(platformId) : newSelectedPlatforms.add(platformId); const parsedAssets = GithubReleaseEnhancer.core.assetFilter.getParsedAssets(); const availableArchsNow = new Set(); const assetsToConsider = newSelectedPlatforms.size > 0 ? parsedAssets.filter(asset => asset.info.platform && newSelectedPlatforms.has(asset.info.platform)) : parsedAssets; assetsToConsider.forEach(asset => { if (asset.info.architecture) availableArchsNow.add(asset.info.architecture); }); const newSelectedArchs = new Set(state.selectedArchs); for (const selected of newSelectedArchs) { if (!availableArchsNow.has(selected)) { newSelectedArchs.delete(selected); } } setState({ selectedPlatforms: newSelectedPlatforms, selectedArchs: newSelectedArchs }); }, toggleArch(archId) { const { state, setState } = GithubReleaseEnhancer.store; const newSelectedArchs = new Set(state.selectedArchs); if (newSelectedArchs.has(archId)) { newSelectedArchs.delete(archId); } else { newSelectedArchs.add(archId); } setState({ selectedArchs: newSelectedArchs }); }, toggleSupplementaryFilter(filterType) { const { setState, state } = GithubReleaseEnhancer.store; const stateKeyMap = { language: 'filterMatchLanguage', resolution: 'filterMatchResolution', keyword: 'hideByKeyword', source: 'hideSourceCode' }; const stateKey = stateKeyMap[filterType]; if (stateKey) { setState({ [stateKey]: !state[stateKey] }); } }, updateAndSaveAllFilters(select) { const { state, setState } = GithubReleaseEnhancer.store; if (select) { const newSelectedPlatforms = new Set(); GithubReleaseEnhancer.config.PLATFORMS.forEach(platform => { if (state.availableFilters.has(platform.id)) { newSelectedPlatforms.add(platform.id); } }); const newSelectedArchs = new Set(); state.masterAvailableArchs.forEach(arch => newSelectedArchs.add(arch)); const newState = { selectedPlatforms: newSelectedPlatforms, selectedArchs: newSelectedArchs }; const supplementaryFilters = ['language', 'resolution', 'keyword', 'source']; supplementaryFilters.forEach(type => { if (state.availableFilters.has(type)) { const stateKey = { language: 'filterMatchLanguage', resolution: 'filterMatchResolution', keyword: 'hideByKeyword', source: 'hideSourceCode' }[type]; if(stateKey) newState[stateKey] = true; } }); setState(newState); } else { setState({ selectedPlatforms: new Set(), selectedArchs: new Set(), filterMatchLanguage: false, filterMatchResolution: false, hideByKeyword: false, hideSourceCode: false, }); } }, toggleDropdown() { const GRE = GithubReleaseEnhancer; const state = GRE.store.state; if (state.isDropdownOpen) { GRE.core.assetFilter.closePopup(); } else { const wrapper = state.filterUIInstance; if (wrapper) { wrapper.classList.add(GRE.config.CLASS_NAMES.PLATFORM_OPTIONS_OPEN); const filterButton = wrapper.querySelector(`.${GRE.config.CLASS_NAMES.FILTER_BUTTON}`); if (filterButton) filterButton.setAttribute('aria-expanded', 'true'); GRE.store.setState({ isDropdownOpen: true }); requestAnimationFrame(() => { const firstOption = wrapper.querySelector(GRE.config.SELECTORS.FIRST_FOCUSABLE_OPTION); if (firstOption) firstOption.focus(); }); } } }, restoreInitialFilters() { const { state, setState } = GithubReleaseEnhancer.store; if (state.initialFilterState) { const clonedInitialState = { ...state.initialFilterState, selectedPlatforms: new Set(state.initialFilterState.selectedPlatforms), selectedArchs: new Set(state.initialFilterState.selectedArchs), }; setState(clonedInitialState); } }, initializeFilterStates() { const { state, setState } = GithubReleaseEnhancer.store; let newFilterState = {}; if (state.isMainReleasesPage) { newFilterState = { selectedPlatforms: new Set(), selectedArchs: new Set(), filterMatchLanguage: false, filterMatchResolution: false, hideByKeyword: true, hideSourceCode: false, }; // [FIXED v2.0.0] 修复偏好筛选模式下的逻辑 } else if (state.filterMode === 'preferred' && Object.keys(state.preferredFilters).length > 0) { const { preferredFilters } = state; const validatedSelectedPlatforms = new Set(); const validatedSelectedArchs = new Set(); // 1. 验证用户偏好的平台在当前 Release 中是否可用 if (preferredFilters.selectedPlatforms) { for (const platform of preferredFilters.selectedPlatforms) { if (state.availableFilters.has(platform)) { validatedSelectedPlatforms.add(platform); } } } // 2. 只有在至少一个偏好平台可用的情况下,才验证并选择偏好的架构 if (validatedSelectedPlatforms.size > 0 && preferredFilters.selectedArchs) { for (const arch of preferredFilters.selectedArchs) { if (state.masterAvailableArchs.has(arch)) { validatedSelectedArchs.add(arch); } } } newFilterState = { ...preferredFilters, selectedPlatforms: validatedSelectedPlatforms, selectedArchs: validatedSelectedArchs, }; } else { // 智能筛选模式 // [FIXED v2.0.1] 修复智能筛选模式下的逻辑 const selectedPlatforms = new Set(); const selectedArchs = new Set(); // 1. 检查用户的当前平台在 Release 中是否可用 if (state.availableFilters.has(state.currentUserPlatform)) { selectedPlatforms.add(state.currentUserPlatform); // 2. 只有在平台匹配后,才检查并选择架构 if (state.availableArchs.has(state.currentUserArchitecture)) { selectedArchs.add(state.currentUserArchitecture); } } newFilterState = { selectedPlatforms, selectedArchs, filterMatchLanguage: state.availableFilters.has('language'), filterMatchResolution: state.availableFilters.has('resolution'), hideByKeyword: state.availableFilters.has('keyword'), hideSourceCode: state.availableFilters.has('source'), }; } const initialSnapshot = { ...newFilterState, selectedPlatforms: new Set(newFilterState.selectedPlatforms), selectedArchs: new Set(newFilterState.selectedArchs) }; setState({ ...newFilterState, initialFilterState: initialSnapshot, }); } }, // ================================================================================= // [模块] 初始化与主函数 (Initialization & Main) // ================================================================================= async init() { try { if (this._observer) { this._observer.disconnect(); this._observer = null; } if (this._globalClickHandler) { document.removeEventListener('click', this._globalClickHandler); this._globalClickHandler = null; } if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; } if (this.notesCollapser) { this.notesCollapser.cleanup(); } document.querySelector(`.${this.config.CLASS_NAMES.FILTER_CONTAINER}`)?.closest('summary')?.querySelector(`.${this.config.CLASS_NAMES.SUMMARY_MARKER_ICON}`)?.remove(); document.querySelector(`.${this.config.CLASS_NAMES.FILTER_CONTAINER}`)?.remove(); document.querySelector(`.${this.config.CLASS_NAMES.SETTINGS_OVERLAY}`)?.remove(); this.store = createStore({ isChinese: false, isTouchDevice: false, selectedPlatforms: new Set(), selectedArchs: new Set(), masterAvailableArchs: new Set(), availableArchs: new Set(), hideByKeyword: true, hideSourceCode: false, currentUserPlatform: 'unknown', currentUserArchitecture: 'unknown', currentUserLanguage: 'unknown', currentUserResolutionCategory: 'unknown', filterMatchLanguage: false, filterMatchResolution: false, isMainReleasesPage: false, filterUIInstance: null, settingsOverlayElement: null, isDropdownOpen: false, availableFilters: new Set(), releaseNotesMaxHeight: 0, hiddenKeywords: [], clickOutsideToCollapse: true, initialFilterState: null, filterMode: 'smart', preferredFilters: {}, }); const { setState, subscribe } = this.store; this.notesCollapser = new ReleaseNotesCollapser(); setState({ isChinese: (navigator.language || navigator.userLanguage).toLowerCase().includes('zh'), isTouchDevice: 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, }); this.settings.loadUserSettings(); this.ui.styles.addGlobalStyles(); this.utils.checkPageType(); if (!this.store.state.isMainReleasesPage) { setState({ currentUserPlatform: this.utils.getCurrentPlatform(), currentUserArchitecture: await this.utils.getCurrentArchitecture(), currentUserLanguage: this.utils.getCurrentLanguage(), currentUserResolutionCategory: this.utils.getCurrentResolutionCategory() }); } this._unsubscribe = subscribe((currentState) => { this.core.assetFilter.updateAllPlatformOptionsVisualState(currentState); this.core.assetFilter.updateAllArchTagsVisualState(currentState); ['language', 'resolution', 'keyword', 'source'].forEach(type => { const stateKeyMap = { language: 'filterMatchLanguage', resolution: 'filterMatchResolution', keyword: 'hideByKeyword', source: 'hideSourceCode' }; this.core.assetFilter.updateSpecificFilterVisualState(type, currentState[stateKeyMap[type]], currentState.filterUIInstance); }); this.core.assetFilter.updateActionButtons(currentState); this.core.assetFilter.updateAvailabilityMarkers(currentState); this.ui.styles.updateMaxHeightVar(currentState.releaseNotesMaxHeight); this.core.releaseNotes.initializeExpansion(); this.core.assetFilter.filterAssets(currentState); this.core.assetFilter.updateFilterCounter(currentState); }); this.ui.theme.watchSystemTheme(); this.core.releaseNotes.initializeExpansion(); if (!this.store.state.isMainReleasesPage) { this.core.assetFilter.initialize(); } else { this.actions.initializeFilterStates(); } this.page.observer.start(); if (!this._isInitialized) { this.page.navigation.setupListeners(); this.settings.registerMenu(); this._isInitialized = true; } this._globalClickHandler = this.core.assetFilter.handleGlobalClick.bind(this.core.assetFilter); document.addEventListener('click', this._globalClickHandler); } catch (error) { console.error(`[GitHub Filter@${location.pathname}] Script initialization error:`, error); } }, }; function waitForElement(selector, callback) { const element = document.querySelector(selector); if (element) { callback(); return; } const observer = new MutationObserver(() => { if (document.querySelector(selector)) { observer.disconnect(); callback(); } }); observer.observe(document.body, { childList: true, subtree: true }); } function main() { waitForElement(GithubReleaseEnhancer.config.SELECTORS.MAIN_REPO_CONTENT, () => { GithubReleaseEnhancer.init(); }); } if (document.readyState === 'interactive' || document.readyState === 'complete') { main(); } else { document.addEventListener('DOMContentLoaded', main, { once: true }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址