您需要先安装一个扩展,例如 篡改猴、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 // @match https://wallhaven.cc/* // @exclude https://wallhaven.cc/w/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @version 1.5 // @author NooScripts // @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. // @license MIT // @icon https://wallhaven.cc/favicon.ico // ==/UserScript== (function() { 'use strict'; // Constants const MIN_THUMB_SIZE = 240; // Minimum thumbnail size const MAX_THUMB_SIZE = 340; // Maximum thumbnail size const GRID_GAP = 2; // Compact gap between thumbnails // Utility function to safely select elements 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; // Clear existing content 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 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}`); this.container.appendChild(label); labels[purity.id] = label; // 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 ? '▶' : '▼'}`; }); }); // 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'; } 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; borderRadius: 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) { console.error('[Wallhaven Lite Script] Initialization failed:', error); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址