您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds fuzzy search button inside input and frequently used filters toolbar above GitHub project filter input.
// ==UserScript== // @name GitHub Project Filter Formatter with Saved Filters // @namespace http://tampermonkey.net/ // @version 2.4 // @description Adds fuzzy search button inside input and frequently used filters toolbar above GitHub project filter input. // @author xiaohaoxing // @match https://github.com/orgs/.*/projects/.* // @match https://github.com/*/*/projects/* // @grant none // @icon https://github.githubassets.com/favicons/favicon.svg // @license MIT // ==/UserScript== (function() { 'use strict'; const INPUT_ID = 'filter-bar-component-input'; const BUTTON_ID = 'tampermonkey-format-title-button'; const BUTTON_TEXT = '模糊'; const SAVE_BUTTON_ID = 'tampermonkey-save-filter-button'; const SAVE_BUTTON_TEXT = '⭐'; const DROPDOWN_ID = 'tampermonkey-saved-filters-dropdown'; const TOOLBAR_ID = 'tampermonkey-filter-toolbar'; const WRAPPER_CLASS = 'tampermonkey-input-wrapper'; const MIN_INPUT_TEXT_AREA_WIDTH = 50; const STORAGE_KEY = 'github-project-saved-filters'; let filterInput = null; let formatButton = null; let saveButton = null; let savedFiltersDropdown = null; let toolbar = null; let inputWrapper = null; let resizeObserver = null; function getSavedFilters() { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : []; } function saveFilter(name, value) { const filters = getSavedFilters(); const newFilter = { name, value, id: Date.now() }; filters.push(newFilter); localStorage.setItem(STORAGE_KEY, JSON.stringify(filters)); updateDropdown(); return newFilter; } function deleteFilter(id) { const filters = getSavedFilters(); const updated = filters.filter(f => f.id !== id); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); updateDropdown(); } function showManageFiltersDialog() { const filters = getSavedFilters(); if (filters.length === 0) { alert('没有保存的筛选条件'); return; } let message = '已保存的筛选条件:\n\n'; filters.forEach((filter, index) => { message += `${index + 1}. ${filter.name}\n 值: ${filter.value}\n\n`; }); message += '\n输入要删除的筛选条件编号(1-' + filters.length + '),或点击取消:'; const input = prompt(message); if (input) { const index = parseInt(input) - 1; if (index >= 0 && index < filters.length) { const filterToDelete = filters[index]; if (confirm(`确定要删除"${filterToDelete.name}"吗?`)) { deleteFilter(filterToDelete.id); alert('筛选条件已删除'); } } else { alert('无效的编号'); } } } function applyFilter(value) { const currentDomInput = document.getElementById(INPUT_ID); if (!currentDomInput) return; filterInput = currentDomInput; const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set; if (valueSetter) { valueSetter.call(filterInput, value); } else { filterInput.value = value; } const inputEvent = new Event('input', { bubbles: true, cancelable: true }); filterInput.dispatchEvent(inputEvent); filterInput.focus(); } function createSaveButton() { const existingButton = document.getElementById(SAVE_BUTTON_ID); if (existingButton) { return existingButton; } const button = document.createElement('button'); button.id = SAVE_BUTTON_ID; button.textContent = SAVE_BUTTON_TEXT; button.type = 'button'; button.title = '保存当前筛选条件'; button.className = 'btn btn-sm'; button.style.fontSize = '12px'; button.style.lineHeight = '18px'; button.style.height = '28px'; // Override specific styles to ensure consistency button.style.display = 'inline-flex'; button.style.alignItems = 'center'; button.style.gap = '4px'; button.style.marginLeft = '0'; button.style.padding = '4px 8px'; button.style.minWidth = 'auto'; button.style.fontWeight = '500'; // Add hover effects button.addEventListener('mouseenter', () => { button.classList.add('btn-hover'); }); button.addEventListener('mouseleave', () => { button.classList.remove('btn-hover'); }); button.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); const currentDomInput = document.getElementById(INPUT_ID); if (!currentDomInput) return; filterInput = currentDomInput; if (filterInput.value.trim() !== '') { const filterName = prompt('请输入筛选条件的名称:', filterInput.value.substring(0, 20)); if (filterName && filterName.trim()) { saveFilter(filterName.trim(), filterInput.value); alert('筛选条件已保存!'); } } else { alert('请先输入筛选条件'); } }); return button; } function createDropdown() { const existingDropdown = document.getElementById(DROPDOWN_ID); if (existingDropdown) { return existingDropdown; } const dropdown = document.createElement('select'); dropdown.id = DROPDOWN_ID; dropdown.className = 'form-select select-sm'; // Override specific styles dropdown.style.minWidth = '120px'; dropdown.style.maxWidth = '200px'; dropdown.style.marginRight = '8px'; // Small gap between dropdown and button dropdown.style.height = '28px'; dropdown.style.fontSize = '12px'; dropdown.style.lineHeight = '18px'; dropdown.style.paddingLeft = '12px'; dropdown.style.paddingRight = '32px'; dropdown.style.border = '1px solid var(--color-border-default, rgba(31, 35, 40, 0.15))'; dropdown.style.borderRadius = '6px'; dropdown.style.backgroundColor = 'var(--color-canvas-default, #ffffff)'; dropdown.style.color = 'var(--color-fg-default, #24292f)'; dropdown.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg width=\'16\' height=\'16\' viewBox=\'0 0 16 16\' fill=\'%23586069\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath d=\'M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z\'/%3E%3C/svg%3E")'; dropdown.style.backgroundRepeat = 'no-repeat'; dropdown.style.backgroundPosition = 'right 8px center'; dropdown.style.backgroundSize = '16px'; dropdown.style.appearance = 'none'; dropdown.style.cursor = 'pointer'; // Add hover effect dropdown.addEventListener('mouseenter', () => { dropdown.style.borderColor = 'var(--color-border-muted, rgba(31, 35, 40, 0.3))'; dropdown.style.backgroundColor = 'var(--color-canvas-subtle, #f6f8fa)'; }); dropdown.addEventListener('mouseleave', () => { dropdown.style.borderColor = 'var(--color-border-default, rgba(31, 35, 40, 0.15))'; dropdown.style.backgroundColor = 'var(--color-canvas-default, #ffffff)'; }); // Add focus styles dropdown.addEventListener('focus', () => { dropdown.style.outline = '2px solid var(--color-accent-fg, #0969da)'; dropdown.style.outlineOffset = '-1px'; }); dropdown.addEventListener('blur', () => { dropdown.style.outline = 'none'; }); dropdown.addEventListener('change', (event) => { const selectedValue = event.target.value; if (!selectedValue) return; if (selectedValue === 'manage') { // Show management dialog showManageFiltersDialog(); } else { const selectedId = parseInt(selectedValue); const filters = getSavedFilters(); const filter = filters.find(f => f.id === selectedId); if (filter) { applyFilter(filter.value); } } dropdown.value = ''; }); updateDropdown(dropdown); return dropdown; } function updateDropdown(dropdown) { dropdown = dropdown || document.getElementById(DROPDOWN_ID); if (!dropdown) return; const filters = getSavedFilters(); dropdown.innerHTML = ''; const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = filters.length > 0 ? '常用筛选...' : '无保存的筛选'; dropdown.appendChild(defaultOption); filters.forEach(filter => { const option = document.createElement('option'); option.value = filter.id; option.textContent = filter.name; option.title = filter.value; dropdown.appendChild(option); }); // Add manage option if there are saved filters if (filters.length > 0) { const separator = document.createElement('option'); separator.disabled = true; separator.textContent = '────────'; dropdown.appendChild(separator); const manageOption = document.createElement('option'); manageOption.value = 'manage'; manageOption.textContent = '🗑️ 管理筛选条件'; manageOption.style.fontStyle = 'italic'; dropdown.appendChild(manageOption); } dropdown.style.display = filters.length > 0 ? 'block' : 'none'; } function createFormatButton() { const existingButton = document.getElementById(BUTTON_ID); if (existingButton) { return existingButton; } const button = document.createElement('button'); button.id = BUTTON_ID; button.textContent = BUTTON_TEXT; button.type = 'button'; button.title = '转换为模糊搜索格式'; button.className = 'btn-sm'; // Style for button inside input with GitHub native look button.style.position = 'absolute'; button.style.top = '50%'; button.style.right = '28px'; button.style.transform = 'translateY(-50%)'; button.style.zIndex = '10'; button.style.padding = '3px 8px'; button.style.fontSize = '12px'; button.style.height = '24px'; button.style.lineHeight = '1'; button.style.display = 'inline-flex'; button.style.alignItems = 'center'; button.style.justifyContent = 'center'; button.style.border = '1px solid var(--color-border-default, rgba(31, 35, 40, 0.15))'; button.style.borderRadius = '6px'; button.style.backgroundColor = 'var(--color-btn-bg, #f6f8fa)'; button.style.color = 'var(--color-fg-default, #24292f)'; button.style.cursor = 'pointer'; button.style.fontWeight = '500'; button.style.whiteSpace = 'nowrap'; button.style.transition = 'background-color 0.2s, border-color 0.2s'; button.addEventListener('mouseenter', () => { button.style.backgroundColor = 'var(--color-btn-hover-bg, #f3f4f6)'; button.style.borderColor = 'var(--color-border-muted, rgba(31, 35, 40, 0.3))'; }); button.addEventListener('mouseleave', () => { button.style.backgroundColor = 'var(--color-btn-bg, #f6f8fa)'; button.style.borderColor = 'var(--color-border-default, rgba(31, 35, 40, 0.15))'; }); button.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); const currentDomInput = document.getElementById(INPUT_ID); if (!currentDomInput) return; filterInput = currentDomInput; if (filterInput.value.trim() !== '') { const originalValue = filterInput.value; const newValue = `title:*${originalValue}*`; applyFilter(newValue); } }); return button; } function createToolbar() { const existingToolbar = document.getElementById(TOOLBAR_ID); if (existingToolbar) { return existingToolbar; } const toolbar = document.createElement('div'); toolbar.id = TOOLBAR_ID; toolbar.style.display = 'flex'; toolbar.style.alignItems = 'center'; toolbar.style.marginBottom = '4px'; toolbar.style.gap = '0'; toolbar.style.paddingLeft = '0'; toolbar.style.paddingRight = '0'; toolbar.style.width = 'auto'; return toolbar; } function adjustLayout() { if (!filterInput || !inputWrapper || !formatButton) return; // Ensure input fills the wrapper and has box-sizing: border-box filterInput.style.width = '100%'; filterInput.style.boxSizing = 'border-box'; // Temporarily show button to measure its width if it was hidden const wasButtonHidden = formatButton.style.display === 'none'; if (wasButtonHidden) { formatButton.style.visibility = 'hidden'; formatButton.style.display = 'inline-flex'; } const buttonActualWidth = formatButton.offsetWidth; if (wasButtonHidden) { formatButton.style.display = 'none'; formatButton.style.visibility = 'visible'; } const wrapperInnerWidth = inputWrapper.clientWidth - (parseFloat(getComputedStyle(inputWrapper).paddingLeft) || 0) - (parseFloat(getComputedStyle(inputWrapper).paddingRight) || 0); const spaceForButtonAndGap = buttonActualWidth + 16; // Button width + gap from right edge // Check if there's enough space for the button AND a minimum text area if (buttonActualWidth > 0 && (wrapperInnerWidth - spaceForButtonAndGap) >= MIN_INPUT_TEXT_AREA_WIDTH) { filterInput.style.paddingRight = `${spaceForButtonAndGap}px`; formatButton.style.display = 'inline-flex'; } else { // Not enough space, hide button and reset padding filterInput.style.paddingRight = '8px'; formatButton.style.display = 'none'; } } function setupInputAndButton() { const currentDomInput = document.getElementById(INPUT_ID); if (!currentDomInput) { // Input disappeared, cleanup if (resizeObserver && inputWrapper) { resizeObserver.unobserve(inputWrapper); } if (formatButton && formatButton.parentNode) formatButton.parentNode.removeChild(formatButton); if (toolbar && toolbar.parentNode) toolbar.parentNode.removeChild(toolbar); if (inputWrapper && inputWrapper.parentNode && inputWrapper.classList.contains(WRAPPER_CLASS)) { const originalParent = inputWrapper.parentNode; if (filterInput && document.body.contains(filterInput) && filterInput.parentElement !== originalParent) { originalParent.insertBefore(filterInput, inputWrapper); filterInput.style.paddingRight = ''; filterInput.style.width = ''; } originalParent.removeChild(inputWrapper); } filterInput = null; inputWrapper = null; formatButton = null; toolbar = null; saveButton = null; savedFiltersDropdown = null; return; } filterInput = currentDomInput; // Setup input wrapper for fuzzy button if (filterInput.parentElement && filterInput.parentElement.classList.contains(WRAPPER_CLASS)) { inputWrapper = filterInput.parentElement; } else { inputWrapper = document.createElement('div'); inputWrapper.classList.add(WRAPPER_CLASS); inputWrapper.style.position = 'relative'; // Mimic original input's display const originalInputComputedStyle = getComputedStyle(filterInput); inputWrapper.style.display = originalInputComputedStyle.display; if (originalInputComputedStyle.display === 'block' || originalInputComputedStyle.display === 'flex') { inputWrapper.style.width = '100%'; } else if (originalInputComputedStyle.display === 'inline-block') { if (originalInputComputedStyle.width !== 'auto' && !originalInputComputedStyle.width.includes('%')) { inputWrapper.style.width = originalInputComputedStyle.width; } } if (filterInput.parentElement && getComputedStyle(filterInput.parentElement).display.includes('flex')) { inputWrapper.style.flexGrow = originalInputComputedStyle.flexGrow; inputWrapper.style.flexShrink = originalInputComputedStyle.flexShrink; inputWrapper.style.flexBasis = originalInputComputedStyle.flexBasis; } if (filterInput.parentNode) { filterInput.parentNode.insertBefore(inputWrapper, filterInput); inputWrapper.appendChild(filterInput); } else { console.warn("[Tampermonkey] Filter input has no parent, cannot wrap."); return; } } // Add fuzzy button to input wrapper if (!formatButton || !inputWrapper.contains(formatButton)) { formatButton = createFormatButton(); inputWrapper.appendChild(formatButton); } // Setup toolbar above input // Find the filter-bar-module__Filter_0--v8FnK div which is the form container let formContainer = filterInput.closest('[id="filter-bar-component"]'); if (!formContainer) { console.warn("[Tampermonkey] Could not find filter-bar-component"); return; } // Find the parent that contains the entire filter section let filterContainer = formContainer.closest('.tokenized-filter-input-module__Box--w5A7b'); if (!filterContainer) { // Try alternative selector for the filter container filterContainer = formContainer.parentElement; if (!filterContainer) { console.warn("[Tampermonkey] Could not find tokenized filter container"); return; } } // Create or find toolbar if (!toolbar || !document.body.contains(toolbar)) { toolbar = createToolbar(); // Insert toolbar before the filter container if (filterContainer.parentNode) { filterContainer.parentNode.insertBefore(toolbar, filterContainer); } else { console.warn("[Tampermonkey] Could not insert toolbar"); return; } } // Add dropdown and save button to toolbar if (!savedFiltersDropdown || !toolbar.contains(savedFiltersDropdown)) { savedFiltersDropdown = createDropdown(); toolbar.appendChild(savedFiltersDropdown); } if (!saveButton || !toolbar.contains(saveButton)) { saveButton = createSaveButton(); toolbar.appendChild(saveButton); } // Initial layout adjustment adjustLayout(); // Observe wrapper for size changes if (typeof ResizeObserver !== 'undefined') { if (resizeObserver) { resizeObserver.disconnect(); } resizeObserver = new ResizeObserver(entries => { requestAnimationFrame(adjustLayout); }); if (inputWrapper) { resizeObserver.observe(inputWrapper); } } // Update dropdown visibility updateDropdown(); } const mutationObserver = new MutationObserver(() => { const currentInput = document.getElementById(INPUT_ID); if (currentInput) { if (!inputWrapper || !inputWrapper.contains(currentInput) || !formatButton || !inputWrapper.contains(formatButton) || !toolbar || !document.body.contains(toolbar)) { setupInputAndButton(); } } else if (filterInput) { setupInputAndButton(); // This will trigger the cleanup logic } }); mutationObserver.observe(document.body, { childList: true, subtree: true }); setTimeout(setupInputAndButton, 1000); })();