您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Organizes thumbnails into collapsible SFW, Sketchy, and NSFW sections with dynamic grid resizing and a floating button to toggle seen wallpapers with persistent state.
// ==UserScript== // @name [Wallhaven] Purity Groups // @namespace NooScripts // @author NooScripts // @version 2.1 // @description Organizes thumbnails into collapsible SFW, Sketchy, and NSFW sections with dynamic grid resizing and a floating button to toggle seen wallpapers with persistent state. // @match https://wallhaven.cc/* // @exclude https://wallhaven.cc/w/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @license MIT // @icon https://wallhaven.cc/favicon.ico // ==/UserScript== (function() { 'use strict'; // Constants const MIN_THUMB_SIZE = 240; // Min thumbnail size const MAX_THUMB_SIZE = 340; // Max thumbnail size const GRID_GAP = 2; const LINK_OPEN_DELAY = 200; // Delay between opening links in ms // Purity colors const PURITY_COLORS = { 'sfw': '#008000', // Green 'sketchy': '#ffa500', // Orange 'nsfw': '#ff0000' // Red }; const $ = (selector, context = document) => context.querySelector(selector); const $$ = (selector, context = document) => context.querySelectorAll(selector); // Debounce utility to limit resize event frequency const debounce = (func, wait) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }; // Grid Grouper Class class GridGrouper { constructor(container) { this.container = container; this.originalOrder = Array.from($$('figure', this.container)); // Bind resize handler this.handleResize = debounce(() => this.groupByPurity(), 100); window.addEventListener('resize', this.handleResize); } calculateColumnsAndThumbSize() { const availableWidth = this.container.clientWidth; // Estimate columns based on minimum thumb size const maxColumns = Math.floor(availableWidth / (MIN_THUMB_SIZE + GRID_GAP)); // Calculate actual thumb size to fill container const totalGapWidth = GRID_GAP * (maxColumns - 1); const thumbSize = Math.min( MAX_THUMB_SIZE, Math.max(MIN_THUMB_SIZE, (availableWidth - totalGapWidth) / maxColumns) ); const columns = Math.max(1, Math.floor(availableWidth / (thumbSize + GRID_GAP))); return { columns, thumbSize }; } groupByPurity() { if (!this.container) return; this.container.innerHTML = ''; // Define purities in desired order with styling const purities = [ { id: 'sfw', title: 'SFW', className: 'purity-sfw' }, { id: 'sketchy', title: 'Sketchy', className: 'purity-sketchy' }, { id: 'nsfw', title: 'NSFW', className: 'purity-nsfw' } ]; const sections = {}; const labels = {}; // Create separate labels and containers for each purity purities.forEach(purity => { // Create a flex container for the header and preview button const headerContainer = document.createElement('div'); headerContainer.className = `header-container-${purity.id}`; headerContainer.id = `header-container-${purity.id}`; headerContainer.style.display = 'flex'; headerContainer.style.alignItems = 'center'; headerContainer.style.width = '100%'; headerContainer.style.boxSizing = 'border-box'; headerContainer.style.marginBottom = '5px'; this.container.appendChild(headerContainer); // Create label as a button const label = document.createElement('button'); label.className = `section-label ${purity.className}-label`; label.textContent = `${purity.title} ▶`; label.setAttribute('aria-expanded', 'true'); label.setAttribute('aria-controls', `section-${purity.id}`); label.style.width = '100%'; label.style.margin = '0'; headerContainer.appendChild(label); labels[purity.id] = label; // Create preview button container const groupwallButtonContainer = document.createElement('div'); groupwallButtonContainer.id = `preview-container-${purity.id}`; groupwallButtonContainer.style.flexGrow = '0'; groupwallButtonContainer.style.marginLeft = '10px'; // Create preview button const groupwallButton = document.createElement('button'); groupwallButton.id = `preview-button-${purity.id}`; groupwallButton.textContent = `Open Unseen`; groupwallButton.style.width = '110px'; groupwallButton.style.padding = '4px 8px'; groupwallButton.style.backgroundColor = PURITY_COLORS[purity.id]; groupwallButton.style.color = 'white'; groupwallButton.style.border = 'none'; groupwallButton.style.borderRadius = '4px'; groupwallButton.style.cursor = 'pointer'; groupwallButton.style.fontSize = '14px'; groupwallButtonContainer.appendChild(groupwallButton); headerContainer.appendChild(groupwallButtonContainer); // Create section container sections[purity.id] = document.createElement('div'); sections[purity.id].className = `purity-section ${purity.className}`; sections[purity.id].id = `section-${purity.id}`; this.container.appendChild(sections[purity.id]); // Add click event to toggle collapse/expand label.addEventListener('click', () => { const isExpanded = label.getAttribute('aria-expanded') === 'true'; sections[purity.id].style.display = isExpanded ? 'none' : 'grid'; label.setAttribute('aria-expanded', !isExpanded); label.textContent = `${purity.title} ${isExpanded ? '▶' : '▼'}`; }); // Add click event for preview button with delay groupwallButton.addEventListener('click', async () => { const thumbs = sections[purity.id].querySelectorAll('.thumb:not(.thumb-seen)'); for (const thumb of thumbs) { const preview = thumb.querySelector('a.preview'); if (preview && preview.href) { window.open(preview.href, '_blank'); await new Promise(resolve => setTimeout(resolve, LINK_OPEN_DELAY)); } } }); }); // Sort wallpapers into sections this.originalOrder.forEach(thumb => { const purity = purities.find(p => thumb.classList.contains(`thumb-${p.id}`))?.id || 'sfw'; sections[purity].appendChild(thumb); }); // Calculate columns and thumb size const { columns, thumbSize } = this.calculateColumnsAndThumbSize(); // Apply grid styling to non-empty sections and hide empty ones purities.forEach(purity => { if (sections[purity.id].children.length === 0) { sections[purity.id].style.display = 'none'; labels[purity.id].style.display = 'none'; labels[purity.id].parentNode.style.display = 'none'; // Hide header container } else { Object.assign(sections[purity.id].style, { display: 'grid', gap: `${GRID_GAP}px`, gridTemplateColumns: `repeat(${columns}, minmax(${MIN_THUMB_SIZE}px, 1fr))` }); } }); // Apply thumbnail styling $$('[data-wallpaper-id]', this.container).forEach(element => { Object.assign(element.style, { width: `${thumbSize}px`, height: `${thumbSize}px` }); const image = $('[data-src]', element); if (image) { Object.assign(image.style, { maxWidth: '100%', maxHeight: '100%', width: '100%', height: '100%', objectFit: 'contain' }); } }); // Ensure container supports vertical stacking Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', gap: '0', width: '100%', boxSizing: 'border-box' }); } // Cleanup event listeners destroy() { window.removeEventListener('resize', this.handleResize); } } // Control Panel Class class ControlPanel { constructor(grouper) { this.grouper = grouper; this.panelId = 'wallhaven-control-panel'; this.seenButtonId = 'toggle-seen-button'; // Initialize hideSeen from saved state this.hideSeen = GM_getValue('hideSeen', false); } createPanel() { const panel = document.createElement('div'); panel.id = this.panelId; panel.innerHTML = ` <div class="control-group"> <button id="${this.seenButtonId}">${this.hideSeen ? 'Show Seen' : 'Hide Seen'}</button> </div> `; document.body.appendChild(panel); // Apply initial visibility state this.applySeenWallpapersState(); // Event Listener const seenButton = $(`#${this.seenButtonId}`); seenButton.addEventListener('click', () => this.toggleSeenWallpapers(seenButton)); } applySeenWallpapersState() { // Remove existing style if present const existingStyle = $(`style[data-id="hide-seen-style"]`); if (existingStyle) existingStyle.remove(); // Apply or remove visibility for seen wallpapers const seenThumbs = $$('figure.thumb.thumb-seen'); if (this.hideSeen) { // Inject CSS rule GM_addStyle(` figure.thumb.thumb-seen { display: none !important; } `).setAttribute('data-id', 'hide-seen-style'); // Fallback: directly set style seenThumbs.forEach(thumb => { thumb.style.display = 'none'; }); } else { // Restore default visibility seenThumbs.forEach(thumb => { thumb.style.display = ''; }); } // Re-run grouping to update section visibility this.grouper.groupByPurity(); } toggleSeenWallpapers(button) { // Toggle the state this.hideSeen = !this.hideSeen; // Save the new state GM_setValue('hideSeen', this.hideSeen); // Update button text button.textContent = this.hideSeen ? 'Show Seen' : 'Hide Seen'; // Apply visibility state this.applySeenWallpapersState(); } init() { this.createPanel(); const panel = $(`#${this.panelId}`); if (panel) { Object.assign(panel.style, { position: 'fixed', bottom: '10px', right: '10px', backgroundColor: 'rgba(0, 0, 0, 0.9)', padding: '12px', border: '1px solid #666', borderRadius: '10px', zIndex: '9999' }); } } } // Styles GM_addStyle(` #wallhaven-control-panel { display: flex; flex-direction: column; min-width: 50px; } .control-group { align-items: center; } #toggle-seen-button { padding: 4px 10px; background-color: #444; color: #fff; border: 1px solid #666; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background-color 0.2s; } #toggle-seen-button:hover { background-color: #555; } .thumb-listing .thumb, .thumb-listing-page .thumb { margin: 1px; } .thumb-listing-page { flex-direction: column !important; gap: ${GRID_GAP}px; width: 100%; padding: 0 10px; box-sizing: border-box; } .purity-section { width: 100%; box-sizing: border-box; padding: 15px; margin-bottom: 20px; border-radius: 8px; transition: transform 0.2s; } .purity-sfw { border: 2px solid #008000; } .purity-sketchy { border: 2px solid #ffa500; } .purity-nsfw { border: 2px solid #ff0000; } .section-label { background-color: #333; color: #fff; font-size: 18px; padding: 8px 12px; margin: 10px 0 5px 0; border: none; border-radius: 4px; text-transform: uppercase; letter-spacing: 1px; cursor: pointer; text-align: left; width: 100%; box-sizing: border-box; transition: background-color 0.2s; } .section-label:hover { background-color: #444; } .purity-sfw-label { color: #00cc00; } .purity-sketchy-label { color: #ffcc00; } .purity-nsfw-label { color: #ff3333; } @media (max-width: 600px) { .section-label { font-size: 16px; padding: 6px 10px; } .purity-section { padding: 10px; } } `); // Initialize try { const container = $('.thumb-listing-page'); if (container) { const grouper = new GridGrouper(container); new ControlPanel(grouper).init(); } } catch (error) { // Silently handle errors } })();