// ==UserScript==
// @name GitHub发布平台筛选器
// @name:en GitHub Release Platform Filter
// @namespace http://tampermonkey.net/
// @version 0.3.1
// @description 筛选GitHub发布资源的平台,优化发布说明显示
// @description:en Filter GitHub release assets by platform and optimize release notes display
// @author EEP
// @match https://github.com/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
// 添加调试信息
const debug = {
log: function (message) {
console.log('[GitHub Release Filter]', message);
},
};
debug.log('脚本开始加载');
// 样式定义
const styles = `
.markdown-body.my-3 {
position: relative;
max-height: 300px;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
padding-bottom: 40px;
}
.markdown-body.my-3.expanded {
max-height: none;
padding-bottom: 40px;
}
.toggle-button {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
padding: 10px;
cursor: pointer;
color: #0366d6;
font-weight: 600;
width: 100%;
margin: 0;
height: 40px;
box-sizing: border-box;
background: none;
}
.markdown-body.my-3:not(.expanded) .toggle-button {
background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
}
.markdown-body.my-3.expanded .toggle-button {
border-top: 1px solid #e1e4e8;
}
.toggle-button:hover {
text-decoration: underline;
}
.platform-filter {
position: fixed;
top: 60px;
right: 16px;
z-index: 1000;
background: white;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.platform-filter-button {
padding: 8px 12px;
border: 1px solid #d1d5da;
border-radius: 6px;
background-color: #f6f8fa;
color: #24292e;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
.platform-filter-button:hover {
background-color: #f3f4f6;
border-color: #bbb;
}
.platform-filter-button svg {
width: 16px;
height: 16px;
}
.platform-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: white;
border: 1px solid #e1e4e8;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(149,157,165,0.2);
display: none;
min-width: 200px;
}
.platform-dropdown.show {
display: block;
}
.platform-option {
padding: 8px 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.platform-option:hover {
background-color: #f6f8fa;
}
.platform-option.selected {
background-color: #f1f8ff;
color: #0366d6;
}
.platform-option svg {
width: 16px;
height: 16px;
}
.Box-row.d-flex.flex-column.flex-md-row.hidden-asset {
display: none !important;
}
`;
// 添加SVG图标
const ICONS = {
settings: ``,
};
// 主要功能实现
function initFeaturesPanels() {
debug.log('开始初始化面板');
const panels = document.querySelectorAll('.markdown-body.my-3');
debug.log(`找到 ${panels.length} 个面板`);
panels.forEach((panel) => {
// 检查内容高度是否需要添加展开/收起功能
if (panel.scrollHeight > 300) {
// 检查是否已经存在按钮
let toggleButton = panel.querySelector('.toggle-button');
if (!toggleButton) {
// 如果不存在按钮,则创建新按钮
toggleButton = document.createElement('div');
toggleButton.className = 'toggle-button';
panel.appendChild(toggleButton);
}
// 根据当前状态设置按钮文本
const isExpanded = panel.classList.contains('expanded');
toggleButton.textContent = isExpanded ? '收起' : '展开更多';
// 更新或添加点击事件
toggleButton.onclick = () => {
const isExpanded = panel.classList.contains('expanded');
panel.classList.toggle('expanded');
toggleButton.textContent = !isExpanded ? '收起' : '展开更多';
if (!isExpanded) {
panel.scrollIntoView({ behavior: 'smooth' });
}
};
}
});
}
// 监听页面变化,确保在动态加载的内容上也能生效
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
const hasMarkdownBody = Array.from(mutation.addedNodes).some(
(node) =>
node.nodeType === 1 &&
(node.classList?.contains('markdown-body') ||
node.querySelector?.('.markdown-body'))
);
if (hasMarkdownBody) {
debug.log('检测到新的markdown内容,重新初始化面板');
initFeaturesPanels();
break;
}
}
}
});
// 获取当前系统平台
function getCurrentPlatform() {
const platform = navigator.platform.toLowerCase();
const userAgent = navigator.userAgent.toLowerCase();
if (platform.includes('win')) return 'windows';
if (platform.includes('mac')) return 'macos';
if (platform.includes('linux')) return 'linux';
if (userAgent.includes('android')) return 'android';
return 'unknown';
}
// 判断资源是否属于指定平台
function isAssetForPlatform(assetText, platform) {
const text = assetText.toLowerCase();
// 检查是否是md5文件或源代码文件
const isMd5File = text.includes('md5') || text.endsWith('.md5');
const isSourceCode =
text.includes('source') ||
text.includes('src') ||
text.endsWith('.zip') ||
text.endsWith('.tar.gz');
// 如果是md5文件,根据文件名中的平台信息判断
if (isMd5File) {
switch (platform) {
case 'windows':
return text.includes('win') || text.includes('windows');
case 'macos':
return (
text.includes('mac') ||
text.includes('darwin') ||
text.includes('osx')
);
case 'linux':
return text.includes('linux') || text.includes('musl');
case 'android':
return text.includes('android');
default:
return false;
}
}
// 如果是源代码文件,对所有平台都显示
if (isSourceCode) {
return true;
}
// 其他文件按照原有逻辑判断
switch (platform) {
case 'windows':
return (
text.includes('windows') ||
text.includes('.exe') ||
text.includes('.msi') ||
text.includes('win') ||
text.includes('win32') ||
text.includes('win64')
);
case 'macos':
return (
text.includes('macos') ||
text.includes('darwin') ||
text.includes('.dmg') ||
text.includes('mac') ||
text.includes('osx') ||
text.includes('apple') ||
text.includes('x64.pkg') ||
text.includes('arm64.pkg')
);
case 'linux':
return (
text.includes('linux') ||
text.includes('.deb') ||
text.includes('.rpm') ||
text.includes('.appimage') ||
text.includes('x86_64') ||
text.includes('amd64') ||
text.includes('arm64')
);
case 'android':
return (
text.includes('android') ||
text.includes('.apk') ||
text.includes('.aab') ||
text.includes('arm') ||
text.includes('aarch64')
);
default:
return false;
}
}
// 获取当前系统平台
const currentPlatform = getCurrentPlatform();
// 获取保存的平台选择,如果没有保存过,则只选中当前平台
let selectedPlatforms = GM_getValue('selectedPlatforms', null);
if (selectedPlatforms === null) {
selectedPlatforms = [currentPlatform];
GM_setValue('selectedPlatforms', selectedPlatforms);
}
// 筛选资源
function filterAssets() {
const assetsContainer = document.querySelector('.Box--condensed');
if (!assetsContainer) return;
const assetsList = assetsContainer.querySelectorAll(
'.Box-row.d-flex.flex-column.flex-md-row'
);
if (selectedPlatforms.length === 0) {
// 如果没有选择任何平台,显示所有资源
assetsList.forEach((asset) => {
asset.classList.remove('hidden-asset');
});
return;
}
assetsList.forEach((asset) => {
const assetText = asset.textContent;
const shouldShow = selectedPlatforms.some((platform) =>
isAssetForPlatform(assetText, platform)
);
asset.classList.toggle('hidden-asset', !shouldShow);
});
debug.log(`筛选完成,当前选中平台: ${selectedPlatforms.join(', ')}`);
}
// 初始化资源列表筛选
function initAssetsFilter() {
debug.log('开始初始化资源列表筛选');
const assetsContainer = document.querySelector('.Box--condensed');
if (!assetsContainer) {
debug.log('未找到资源容器,等待100ms后重试');
setTimeout(initAssetsFilter, 100);
return;
}
// 检查是否已经存在筛选器
if (document.querySelector('.platform-filter')) {
debug.log('筛选器已存在,重新应用筛选规则');
filterAssets();
return;
}
// 检查是否有资源列表
const assetsList = assetsContainer.querySelectorAll(
'.Box-row.d-flex.flex-column.flex-md-row'
);
if (assetsList.length === 0) {
debug.log('未找到资源列表,等待100ms后重试');
setTimeout(initAssetsFilter, 100);
return;
}
debug.log(`找到 ${assetsList.length} 个资源项`);
// 创建一个 MutationObserver 来监听资源列表的变化
const assetsObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
const hasAssets = Array.from(mutation.addedNodes).some(
(node) => node.nodeType === 1 && node.classList?.contains('Box-row')
);
if (hasAssets) {
debug.log('检测到新的资源列表,重新应用筛选');
filterAssets();
}
}
}
});
// 开始观察资源列表容器
assetsObserver.observe(assetsContainer, {
childList: true,
subtree: true,
});
// 创建筛选器容器
const filterContainer = document.createElement('div');
filterContainer.className = 'platform-filter';
// 创建筛选按钮
const filterButton = document.createElement('button');
filterButton.className = 'platform-filter-button';
filterButton.innerHTML = `${ICONS.settings} 平台筛选`;
// 创建下拉菜单
const dropdown = document.createElement('div');
dropdown.className = 'platform-dropdown';
// 添加平台选项
const platforms = [
{ id: 'windows', name: 'Windows' },
{ id: 'macos', name: 'macOS' },
{ id: 'linux', name: 'Linux' },
{ id: 'android', name: 'Android' },
];
platforms.forEach((platform) => {
const option = document.createElement('div');
option.className = `platform-option ${
selectedPlatforms.includes(platform.id) ? 'selected' : ''
}`;
option.textContent = platform.name;
option.onclick = (e) => {
e.stopPropagation();
const index = selectedPlatforms.indexOf(platform.id);
if (index === -1) {
selectedPlatforms.push(platform.id);
} else {
selectedPlatforms.splice(index, 1);
}
option.classList.toggle('selected');
GM_setValue('selectedPlatforms', selectedPlatforms);
filterAssets();
};
dropdown.appendChild(option);
});
// 添加按钮点击事件
filterButton.onclick = () => {
dropdown.classList.toggle('show');
};
// 点击其他地方关闭下拉菜单
document.addEventListener('click', (e) => {
if (!filterContainer.contains(e.target)) {
dropdown.classList.remove('show');
}
});
// 组装并添加到页面
filterContainer.appendChild(filterButton);
filterContainer.appendChild(dropdown);
const mainContent = document.querySelector('main');
const releaseHeader = document.querySelector('.release-header');
if (releaseHeader) {
releaseHeader.style.position = 'relative';
releaseHeader.appendChild(filterContainer);
} else if (mainContent) {
mainContent.appendChild(filterContainer);
} else {
document.body.appendChild(filterContainer);
}
// 初始筛选
filterAssets();
}
// 添加样式到页面
function addStyles() {
debug.log('添加样式到页面');
if (!document.querySelector('style[data-github-release-filter]')) {
const styleElement = document.createElement('style');
styleElement.setAttribute('data-github-release-filter', 'true');
styleElement.textContent = styles;
document.head.appendChild(styleElement);
}
}
// 检查是否在releases页面
function isReleasesPage() {
return window.location.href.toLowerCase().includes('releases');
}
// 等待页面加载完成后再初始化
function initializeFeatures() {
debug.log('检查页面类型');
if (!isReleasesPage()) {
debug.log('当前不是releases页面,跳过初始化');
return;
}
debug.log('开始初始化所有功能');
// 确保样式已添加
if (!document.querySelector('style[data-github-release-filter]')) {
addStyles();
}
// 先初始化发布说明功能
initFeaturesPanels();
startObserver();
// 然后初始化资源列表筛选功能
initAssetsFilter();
}
// 检查并初始化功能
function checkAndInitialize() {
debug.log('检查页面状态并初始化功能');
if (!isReleasesPage()) {
debug.log('当前不是releases页面,跳过初始化');
return;
}
// 确保样式已添加
addStyles();
// 确保页面已经完全加载
if (
document.readyState === 'complete' ||
document.readyState === 'interactive'
) {
// 先初始化发布说明功能
const markdownContent = document.querySelector('.markdown-body.my-3');
if (markdownContent) {
debug.log('找到发布说明内容,开始初始化');
addStyles();
initFeaturesPanels();
startObserver();
}
// 检查资源列表是否存在
const releaseContent = document.querySelector('.Box--condensed');
if (releaseContent) {
debug.log('找到资源列表,开始初始化');
// 确保所有动态内容都已加载
setTimeout(() => {
initAssetsFilter();
// 持续监听可能的动态内容加载
const contentObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (
mutation.type === 'childList' &&
mutation.addedNodes.length > 0
) {
const hasMarkdownBody = Array.from(mutation.addedNodes).some(
(node) =>
node.nodeType === 1 &&
(node.classList?.contains('markdown-body') ||
node.querySelector?.('.markdown-body'))
);
if (hasMarkdownBody) {
initFeaturesPanels();
}
const hasAssets = Array.from(mutation.addedNodes).some(
(node) =>
node.nodeType === 1 && node.classList?.contains('Box-row')
);
if (hasAssets) {
initAssetsFilter();
}
}
}
});
contentObserver.observe(releaseContent, {
childList: true,
subtree: true,
});
}, 500);
} else {
debug.log('未找到资源列表,等待100ms后重试');
setTimeout(checkAndInitialize, 100);
}
} else {
debug.log('页面未完全加载,等待加载完成');
window.addEventListener('load', checkAndInitialize);
}
}
// 初始检查
checkAndInitialize();
// 监听Turbo Drive页面切换事件
document.addEventListener('turbo:load', () => {
debug.log('检测到页面切换,重新检查并初始化功能');
checkAndInitialize();
});
// 监听导航事件
window.addEventListener('popstate', () => {
debug.log('检测到导航事件,重新检查并初始化功能');
checkAndInitialize();
});
function startObserver() {
debug.log('开始观察页面变化');
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
})();