您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filters albums, infinite scroll, duration filtering on album pages, hide viewed albums
// ==UserScript== // @name Erome Enhancer (beta) // @namespace http://violentmonkey.net/ // @version 2.9 // @license MIT // @author LisaTurtlesCuck // @description Filters albums, infinite scroll, duration filtering on album pages, hide viewed albums // @match https://www.erome.com/a // @match https://www.erome.com/explore* // @match https://www.erome.com/search* // @match https://www.erome.com/user/feed* // @match https://www.erome.com/user/liked* // @match https://www.erome.com/user/saved* // @match https://www.erome.com/* // @grant none // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'eromeEnhancerSettings'; const VIEWED_KEY = 'eromeViewedAlbums'; const DEFAULTS = { filterMode: 'videos', // 'videos' | 'images' | 'all' autoScroll: true, hideViewed: false, minVideoSeconds: 0, // album: hide videos shorter than this (0 = off) }; let settings = loadSettings(); let viewedAlbums = loadViewed(); let currentPage = 1; let loading = false; const MAX_PAGES = 50; /* ---------- Storage ---------- */ function loadSettings() { try { return Object.assign({}, DEFAULTS, JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')); } catch { return Object.assign({}, DEFAULTS); } } function saveSettings() { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } function loadViewed() { try { return JSON.parse(localStorage.getItem(VIEWED_KEY) || '[]'); } catch { return []; } } function saveViewed() { localStorage.setItem(VIEWED_KEY, JSON.stringify(viewedAlbums)); } function clearViewed() { viewedAlbums = []; saveViewed(); alert('Viewed albums cleared!'); location.reload(); } /* ---------- Utilities ---------- */ function parseDurationText(text) { if (!text) return 0; console.log('Parsing duration text:', text); const parts = text.trim().split(':').map(Number); if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; if (parts.length === 2) return parts[0] * 60 + parts[1]; return Number(parts[0]) || 0; } function fixLazyImages(root = document) { root.querySelectorAll('img').forEach(img => { if (img.dataset.src) img.src = img.dataset.src; if (img.dataset.srcset) img.srcset = img.dataset.srcset; if (img.getAttribute('data-src')) img.src = img.getAttribute('data-src'); if (img.getAttribute('data-srcset')) img.srcset = img.getAttribute('data-srcset'); img.classList.remove('lazy', 'lazyload', 'lozad'); }); } /* ---------- Grid Filtering (FIXED) ---------- */ function matchesFilter(albumEl) { const videoSpan = albumEl.querySelector('.album-videos'); const imageSpan = albumEl.querySelector('.album-images'); const vCount = videoSpan ? Number((videoSpan.textContent.match(/\d+/) || [0])[0]) : 0; const iCount = imageSpan ? Number((imageSpan.textContent.match(/\d+/) || [0])[0]) : 0; const anchor = albumEl.querySelector('a'); const url = anchor ? anchor.href : null; if (settings.hideViewed && url && viewedAlbums.includes(url)) return false; switch (settings.filterMode) { case 'videos': return vCount > 0; case 'images': return iCount > 0 && vCount === 0; default: return true; } } function markAlbumClick(albumEl) { const link = albumEl.querySelector('a'); if (!link || link.dataset.eeBound) return; link.dataset.eeBound = '1'; // FIX: Listen for 'mousedown' and include all buttons (0, 1, 2) link.addEventListener('mousedown', (event) => { // Button 0: Left-click (direct navigation) // Button 1: Middle-click / Scroll Wheel (open in new tab) // Button 2: Right-click (used to open context menu, where 'open in new tab' is selected) if (event.button === 0 || event.button === 1 || event.button === 2) { if (!viewedAlbums.includes(link.href)) { viewedAlbums.push(link.href); saveViewed(); console.log(`Album marked as viewed: ${link.href} (Button: ${event.button})`); } } }); } function applyInitialFilter() { const container = document.querySelector('#albums'); if (!container) return; Array.from(container.querySelectorAll('.album')).forEach(album => { if (!matchesFilter(album)) album.remove(); else { fixLazyImages(album); markAlbumClick(album); } }); } /* ---------- Infinite Scroll ---------- */ async function loadNextPage() { if (loading || currentPage >= MAX_PAGES) return; loading = true; const nextPage = currentPage + 1; const path = location.pathname; let url = `?page=${nextPage}`; if (path.startsWith('/explore')) url = `/explore?page=${nextPage}`; else if (path.startsWith('/search')) { const p = new URLSearchParams(location.search); p.set('page', nextPage); url = `/search?${p.toString()}`; } else if (path.startsWith('/user/feed')) url = `/user/feed?page=${nextPage}`; else if (path.startsWith('/user/liked')) url = `/user/liked?page=${nextPage}`; else if (path.startsWith('/user/saved')) url = `/user/saved?page=${nextPage}`; else if (/^\/[^/]+$/.test(path)) url = `${path}?page=${nextPage}`; try { const res = await fetch(url); const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); fixLazyImages(doc); const newAlbums = doc.querySelectorAll('.album'); const container = document.querySelector('#albums'); if (container) { newAlbums.forEach(n => { const clone = document.importNode(n, true); if (matchesFilter(clone)) { fixLazyImages(clone); markAlbumClick(clone); container.appendChild(clone); } }); } currentPage = nextPage; } catch (err) { console.error('loadNextPage error', err); } loading = false; } function setupInfiniteScroll() { window.addEventListener('scroll', () => { if (!settings.autoScroll) return; if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 800) { loadNextPage(); } }); } /* ---------- Album Pages (duration filtering) ---------- */ function getMediaGroups() { // FIXED: Use correct selectors for album pages return Array.from(document.querySelectorAll('.media-group, .album-media, [class*="media"]')); } function isVideoGroup(g) { // FIXED: Better video detection return !!(g.querySelector('.duration, video, [class*="video"], .fa-video')); } function getGroupDurationSeconds(g) { // FIXED: More robust duration extraction let durationText = ''; // Try direct duration element first const durationEl = g.querySelector('.duration'); if (durationEl) { durationText = durationEl.textContent || durationEl.innerText || ''; console.log('Found duration element:', durationText); } // Try data attributes if (!durationText && g.dataset.duration) { durationText = g.dataset.duration; } // Try HTML data attribute if (!durationText && g.getAttribute('data-duration')) { durationText = g.getAttribute('data-duration'); } // Fallback: search in inner HTML if (!durationText) { const html = g.innerHTML; const durationMatch = html.match(/(\d{1,2}):(\d{2})(?::(\d{2}))?/); if (durationMatch) { durationText = durationMatch[0]; } } // Clean up the text durationText = (durationText || '').trim(); console.log('Final duration text:', durationText, 'for element:', g); return parseDurationText(durationText); } function updateHiddenCounter(n) { const num = document.getElementById('eeCountNum'); if (num) num.textContent = String(n); } function applyAlbumEnhancements() { // FIXED: Don't use #albums on album pages if (!location.pathname.startsWith('/a/')) return; const groups = getMediaGroups(); console.log('Found media groups:', groups.length); if (!groups.length) { console.log('No media groups found'); updateHiddenCounter(0); return; } let hidden = 0; groups.forEach(g => { if (isVideoGroup(g)) { const secs = getGroupDurationSeconds(g); console.log('Video duration:', secs, 'seconds'); if (settings.minVideoSeconds > 0 && secs > 0 && secs < settings.minVideoSeconds) { g.style.display = 'none'; hidden++; console.log('Hiding video with duration:', secs, 'seconds'); } else { g.style.display = ''; } } else { g.style.display = ''; } }); updateHiddenCounter(hidden); console.log('Duration filter applied:', hidden, 'videos hidden'); } function observeAlbumChanges() { if (!location.pathname.startsWith('/a/')) return; // FIXED: Use body or main container for album pages const container = document.body; if (!container) return; const mo = new MutationObserver(() => { clearTimeout(window.__ee_album_timeout); window.__ee_album_timeout = setTimeout(() => { applyAlbumEnhancements(); }, 500); }); mo.observe(container, { childList: true, subtree: true }); } /* ---------- UI ---------- */ function ensureEnhancerNav() { const nav = document.querySelector('.nav.navbar-nav.navbar-right') || document.querySelector('.navbar-right .nav'); if (!nav) return null; if (document.getElementById('enhancerNavItem')) return document.getElementById('enhancerBtn'); const li = document.createElement('li'); li.id = 'enhancerNavItem'; li.innerHTML = ` <a href="#" id="enhancerBtn" style="display:inline-flex;align-items:center;gap:8px;"> <i class="fa fa-sliders"></i> <span>Enhancer</span> <span style="display:inline-flex;align-items:center;gap:4px;color:#eb6395;margin-left:8px;font-weight:600;font-size:13px;"> <i class="fa fa-eye-slash"></i> <span id="eeCountNum">0</span> </span> </a>`; nav.insertBefore(li, nav.firstChild); return document.getElementById('enhancerBtn'); } function addSettingsUI() { const anchor = ensureEnhancerNav(); if (!anchor) return; if (document.getElementById('enhancerModal')) return; const modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = 'enhancerModal'; // Option 1 - Comprehensive Premium Design modal.innerHTML = ` <div class="modal-dialog"> <div class="modal-content" style="background:#2b2b2b;color:#fff;"> <div class="modal-header" style="border-bottom:1px solid #444;padding:20px 25px 15px;"> <button type="button" class="close" data-dismiss="modal" style="color:#fff;opacity:0.8;font-size:24px;margin-top:-5px;">×</button> <h4 class="modal-title" style="font-weight:600;font-size:18px;"> <i class="fa fa-sliders" style="margin-right:10px;color:#eb6395;"></i>Erome Enhancer Settings </h4> </div> <div class="modal-body" style="padding:25px;"> <!-- Main Content Filters --> <div class="settings-section"> <div class="section-header"> <i class="fa fa-th-large" style="margin-right:8px;"></i>Grid View Filters </div> <div class="section-content"> <div class="form-group"> <label class="control-label">Content Filter</label> <select id="filterMode" class="form-control"> <option value="all">Show All Albums</option> <option value="videos">Videos Only</option> <option value="images">Images Only (No Videos)</option> </select> </div> <div class="form-group"> <div class="checkbox"> <label> <input type="checkbox" id="autoScroll"> Auto-load pages (infinite scroll) </label> </div> </div> <div class="form-group"> <div class="checkbox"> <label> <input type="checkbox" id="hideViewed"> Hide viewed albums </label> </div> </div> </div> </div> <hr style="border-color:#444;margin:25px 0;"> <!-- Album Page Filters --> <div class="settings-section"> <div class="section-header"> <i class="fa fa-film" style="margin-right:8px;"></i>Album Page Filters </div> <div class="section-content" style="background:#333;padding:20px;border-radius:8px;margin-top:12px;border:1px solid #444;"> <div class="form-group" style="margin-bottom:20px;"> <label class="control-label" style="font-size:14px;color:#ddd;font-weight:500;"> <i class="fa fa-clock-o" style="margin-right:6px;"></i>Minimum Video Duration </label> <div style="display:flex;align-items:center;gap:12px;margin-top:8px;"> <input type="number" id="minVideoSeconds" class="form-control" min="0" placeholder="0 = disabled" style="flex:1;background:#444;border:1px solid #555;color:#fff;"> <span style="color:#888;font-size:13px;white-space:nowrap;font-weight:500;">seconds</span> </div> <div style="font-size:12px;color:#777;margin-top:8px;line-height:1.4;"> <i class="fa fa-info-circle" style="margin-right:5px;"></i> Videos shorter than this duration will be hidden on album pages </div> </div> <div style="background:#3a3a3a;padding:12px 15px;border-radius:6px;margin-top:15px;border-left:3px solid #eb6395;"> <div style="font-size:12px;color:#999;display:flex;align-items:center;"> <i class="fa fa-exclamation-circle" style="margin-right:8px;font-size:14px;"></i> <span>These settings only apply when viewing individual albums</span> </div> </div> </div> </div> <!-- Debug Info <div style="margin-top:20px;padding:12px 15px;background:#333;border-radius:6px;border:1px solid #444;"> <div style="font-size:12px;color:#888;text-align:center;"> <i class="fa fa-bug" style="margin-right:6px;"></i> Check browser console (F12) for detailed debug information </div> </div> --> </div> <div class="modal-footer" style="border-top:1px solid #444;padding:20px 25px;"> <div style="display:flex;justify-content:space-between;align-items:center;width:100%;"> <div style="display:flex;gap:10px;"> <button id="clearViewed" class="btn btn-default" style="background:#555;border-color:#666;color:#ccc;padding:8px 16px;"> <i class="fa fa-trash" style="margin-right:6px;"></i>Clear Viewed </button> <button id="resetAlbumFilters" class="btn btn-default" style="border-color:#eb6395;color:#eb6395;background:transparent;padding:8px 16px;"> <i class="fa fa-refresh" style="margin-right:6px;"></i>Reset Filters </button> </div> <button id="saveEnhancer" class="btn btn-primary" style="background:#eb6395;border-color:#eb6395;font-weight:600;padding:10px 20px;"> <i class="fa fa-check" style="margin-right:8px;"></i>Apply Settings </button> </div> </div> </div> </div>`; document.body.appendChild(modal); // Add comprehensive CSS styles const style = document.createElement('style'); style.textContent = ` .settings-section { margin-bottom: 25px; } .section-header { font-weight: 600; color: #eb6395; font-size: 15px; margin-bottom: 15px; padding-bottom: 8px; border-bottom: 2px solid #444; display: flex; align-items: center; } .section-content { padding-left: 10px; } .form-group { margin-bottom: 20px; } .control-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: #ddd; } .form-control { background: #444; border: 1px solid #555; color: #fff; border-radius: 6px; padding: 10px 14px; font-size: 14px; transition: all 0.3s ease; } .form-control:focus { border-color: #eb6395; box-shadow: 0 0 0 3px rgba(235, 99, 149, 0.2); background: #444; color: #fff; outline: none; } .checkbox { margin-bottom: 12px; } .checkbox label { display: flex; align-items: center; font-size: 14px; color: #ddd; cursor: pointer; transition: color 0.2s ease; } .checkbox label:hover { color: #fff; } .checkbox input[type="checkbox"] { margin-right: 10px; transform: scale(1.2); accent-color: #eb6395; } .btn { border-radius: 6px; font-size: 13px; padding: 10px 18px; transition: all 0.3s ease; border: none; cursor: pointer; font-weight: 500; } .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.4); } #clearViewed:hover { background: #666 !important; border-color: #777 !important; } #resetAlbumFilters:hover { background: rgba(235, 99, 149, 0.15) !important; box-shadow: 0 4px 12px rgba(235, 99, 149, 0.2); } #saveEnhancer:hover { background: #d85585 !important; border-color: #d85585 !important; box-shadow: 0 6px 16px rgba(235, 99, 149, 0.4); } .modal-content { border-radius: 12px; box-shadow: 0 15px 40px rgba(0,0,0,0.6); border: 1px solid #444; } .modal-header { background: linear-gradient(135deg, #2b2b2b 0%, #333 100%); } .modal-body { background: linear-gradient(135deg, #2b2b2b 0%, #2f2f2f 100%); } `; document.head.appendChild(style); anchor.addEventListener('click', e => { e.preventDefault(); // Initialize modal values document.getElementById('filterMode').value = settings.filterMode; document.getElementById('autoScroll').checked = settings.autoScroll; document.getElementById('hideViewed').checked = settings.hideViewed; document.getElementById('minVideoSeconds').value = settings.minVideoSeconds || 0; if (typeof $ === 'function') { $('#enhancerModal').modal('show'); } else { modal.style.display = 'block'; } }); modal.querySelector('#saveEnhancer').addEventListener('click', () => { settings.filterMode = document.getElementById('filterMode').value; settings.autoScroll = document.getElementById('autoScroll').checked; settings.hideViewed = document.getElementById('hideViewed').checked; settings.minVideoSeconds = parseInt(document.getElementById('minVideoSeconds').value) || 0; saveSettings(); if (typeof $ === 'function') { $('#enhancerModal').modal('hide'); } else { modal.style.display = 'none'; } setTimeout(() => { if (location.pathname.startsWith('/a/')) { applyAlbumEnhancements(); } else { location.reload(); } }, 300); }); modal.querySelector('#clearViewed').addEventListener('click', clearViewed); modal.querySelector('#resetAlbumFilters').addEventListener('click', () => { settings.minVideoSeconds = 0; saveSettings(); document.getElementById('minVideoSeconds').value = 0; if (location.pathname.startsWith('/a/')) { applyAlbumEnhancements(); } }); // Close modal when clicking outside modal.addEventListener('click', (e) => { if (e.target === modal) { if (typeof $ === 'function') { $('#enhancerModal').modal('hide'); } else { modal.style.display = 'none'; } } }); } /* ---------- Init ---------- */ function init() { fixLazyImages(); if (location.pathname.startsWith('/a/')) { // FIXED: Longer delay for album pages to ensure media is loaded setTimeout(() => { console.log('Applying album enhancements...'); applyAlbumEnhancements(); observeAlbumChanges(); }, 2000); } else { applyInitialFilter(); setupInfiniteScroll(); } addSettingsUI(); console.log('Erome Enhancer v2.8.8 loaded - Premium UI'); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();