您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Sorts videos on motherless.com with an advanced, panel-based UI. Supports Title, Views, Duration, Uploader, Random, and "Deep Sort" options (Upload Date, Favorites, Comments, Resolution, Engagement). Fetches all gallery pages for comprehensive sorting and provides pagination. Features a sleek dark mode UI.
// ==UserScript== // @name Motherless.com - Advanced Gallery Sorter // @namespace http://tampermonkey.net/ // @version 3.4 // @description Sorts videos on motherless.com with an advanced, panel-based UI. Supports Title, Views, Duration, Uploader, Random, and "Deep Sort" options (Upload Date, Favorites, Comments, Resolution, Engagement). Fetches all gallery pages for comprehensive sorting and provides pagination. Features a sleek dark mode UI. // @author baratheonblight75 // @match *://*.motherless.com/* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function($) { 'use strict'; const VIDEO_ITEM_SELECTOR = 'div.thumb-container.video'; const VIDEOS_PER_PAGE_DISPLAY = 2500; const MAX_CONCURRENT_DEEP_FETCHES = 5; const DEFAULT_SORT_ID = 'views'; const DEFAULT_SORT_DIR = 'desc'; // --- SVG Icons --- const SVG_ICONS = { filter: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M4 6l8 0" /><path d="M16 6l4 0" /><path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M4 12l2 0" /><path d="M10 12l10 0" /><path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M4 18l11 0" /><path d="M19 18l1 0" /></svg>', close: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>', title: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16 8h4l-4 8h4" /><path d="M4 16v-6a2 2 0 1 1 4 0v6" /><path d="M4 13h4" /><path d="M11 12h2" /></svg>', views: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>', duration: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 12l3 2" /><path d="M12 7v5" /></svg>', uploader: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /></svg>', uploadDate: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4" /><path d="M18 18m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M15 3v4" /><path d="M7 3v4" /><path d="M3 11h16" /><path d="M18 16.496v1.504l1 1" /></svg>', favorites: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /></svg>', comments: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9h8" /><path d="M8 13h6" /><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z" /></svg>', resolution: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M14 9v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z" /><path d="M7 15v-6" /><path d="M10 15v-6" /><path d="M7 12h3" /></svg>', random: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 4l3 3l-3 3" /><path d="M18 20l3 -3l-3 -3" /><path d="M3 7h3a5 5 0 0 1 5 5a5 5 0 0 0 5 5h5" /><path d="M21 7h-5a4.978 4.978 0 0 0 -3 1m-4 8a4.984 4.984 0 0 1 -3 1h-3" /></svg>', arrowUp: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M18 11l-6 -6" /><path d="M6 11l6 -6" /></svg>', arrowDown: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M18 13l-6 6" /><path d="M6 13l6 6" /></svg>' }; // --- Sort Definitions --- const SORT_DEFINITIONS = [ { group: "Basic Sorts", id: 'title', label: 'Title', svgIcon: SVG_ICONS.title, defaultDir: 'asc', deepScan: false, keys: { asc: 'titleAZ', desc: 'titleZA' } }, { group: "Basic Sorts", id: 'views', label: 'Views', svgIcon: SVG_ICONS.views, defaultDir: 'desc', deepScan: false, keys: { asc: 'viewsLeast', desc: 'viewsMost' } }, { group: "Basic Sorts", id: 'duration', label: 'Duration', svgIcon: SVG_ICONS.duration, defaultDir: 'desc', deepScan: false, keys: { asc: 'durationShortest', desc: 'durationLongest' } }, { group: "Basic Sorts", id: 'uploader', label: 'Uploader', svgIcon: SVG_ICONS.uploader, defaultDir: 'asc', deepScan: false, keys: { asc: 'uploaderAZ', desc: 'uploaderZA' } }, { group: "Advanced Sorts (Deep Scan)", id: 'uploadDate', label: 'Upload Date', svgIcon: SVG_ICONS.uploadDate, defaultDir: 'desc', deepScan: true, keys: { asc: 'deep:uploadDateOldest', desc: 'deep:uploadDateNewest' } }, { group: "Advanced Sorts (Deep Scan)", id: 'favorites', label: 'Favorites', svgIcon: SVG_ICONS.favorites, defaultDir: 'desc', deepScan: true, keys: { asc: 'deep:favoritesLeast', desc: 'deep:favoritesMost' } }, { group: "Advanced Sorts (Deep Scan)", id: 'comments', label: 'Comments', svgIcon: SVG_ICONS.comments, defaultDir: 'desc', deepScan: true, keys: { asc: 'deep:commentsLeast', desc: 'deep:commentsMost' } }, { group: "Advanced Sorts (Deep Scan)", id: 'resolution', label: 'Resolution', svgIcon: SVG_ICONS.resolution, defaultDir: 'desc', deepScan: true, keys: { asc: 'deep:resolutionLowest', desc: 'deep:resolutionHighest' } }, { group: "Engagement Sorts (Deep Scan)", id: 'engagementComments', label: 'Comments/View', svgMain: SVG_ICONS.comments, svgBadge: SVG_ICONS.views, defaultDir: 'desc', deepScan: true, keys: { asc: 'deep:engagementCommentsLeast', desc: 'deep:engagementCommentsMost' } }, { group: "Engagement Sorts (Deep Scan)", id: 'engagementFavorites', label: 'Favorites/View', svgMain: SVG_ICONS.favorites, svgBadge: SVG_ICONS.views, defaultDir: 'desc', deepScan: true, keys: { asc: 'deep:engagementFavoritesLeast', desc: 'deep:engagementFavoritesMost' } }, { group: "Other", id: 'random', label: 'Random', svgIcon: SVG_ICONS.random, noDirection: true, deepScan: false, key: 'random' } ]; // --- Helper Functions --- function parseViews(text) { if (typeof text !== 'string') return 0; text = text.trim().toLowerCase(); let multiplier = 1; if (text.includes('k')) multiplier = 1000; if (text.includes('m')) multiplier = 1000000; let number = parseFloat(text.replace(/[^0-9.]/g, '')); return isNaN(number) ? 0 : Math.round(number * multiplier); } function parseDurationToSeconds(durationStr) { if (!durationStr || typeof durationStr !== 'string') return 0; const parts = durationStr.trim().split(':').map(part => parseInt(part, 10)); let seconds = 0; const validParts = parts.filter(p => !isNaN(p)); if (validParts.length === 2) seconds = validParts[0] * 60 + validParts[1]; else if (validParts.length === 3) seconds = validParts[0] * 3600 + validParts[1] * 60 + validParts[2]; else if (validParts.length === 1) seconds = validParts[0]; return isNaN(seconds) ? 0 : seconds; } function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } // --- Deep Data Parsing Functions --- function parseUploadDate(dateStr) { if (!dateStr || typeof dateStr !== 'string') return null; try { const date = new Date(dateStr.trim()); return isNaN(date.getTime()) ? null : date; } catch (e) { console.warn("Could not parse date:", dateStr, e); return null; } } function parseFavorites(favStr) { if (!favStr || typeof favStr !== 'string') return null; const num = parseInt(favStr.replace(/[^0-9]/g, ''), 10); return isNaN(num) ? null : num; } function parseComments(commentStr) { if (!commentStr || typeof commentStr !== 'string') return null; const num = parseInt(commentStr.replace(/[^0-9]/g, ''), 10); return isNaN(num) ? null : num; } function parseResolution(resStr) { if (!resStr || typeof resStr !== 'string') return null; const num = parseInt(resStr.replace(/[^0-9]/g, ''), 10); return isNaN(num) ? null : num; } // --- UI Styling --- const newStyles = ` :root { --mvs-bg-deep: #1A1A1A; --mvs-bg-panel: #282828; --mvs-bg-header-footer: #333333; --mvs-bg-element: #3C3C3C; --mvs-bg-hover: #454545; --mvs-bg-active: #505050; --mvs-border-strong: #555555; --mvs-border-medium: #4A4A4A; --mvs-border-light: #404040; --mvs-text-primary: #E0E0E0; --mvs-text-secondary: #AAAAAA; --mvs-text-disabled: #777777; --mvs-accent-primary: #606060; --mvs-accent-hover: #707070; } #mvs-sort-toggle-btn { position: fixed; bottom: 20px; right: 20px; z-index: 100000; background-color: var(--mvs-accent-primary); color: var(--mvs-text-primary); border: none; padding: 0; width: 52px; height: 52px; border-radius: 50%; cursor: pointer; box-shadow: 0 3px 10px rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s; } #mvs-sort-toggle-btn:hover { background-color: var(--mvs-accent-hover); transform: translateY(-2px); box-shadow: 0 5px 12px rgba(0,0,0,0.6); } #mvs-sort-toggle-btn svg { width: 28px; height: 28px; stroke: currentColor; } #mvs-sort-panel-container { position: fixed; bottom: 85px; right: 20px; width: 280px; /* Further reduced width for "squished" look */ max-height: auto; /* Height will be determined by content */ background-color: var(--mvs-bg-panel); color: var(--mvs-text-primary); border: 1px solid var(--mvs-border-medium); border-radius: 8px; /* Slightly less rounded for compact look */ box-shadow: 0 4px 15px rgba(0,0,0,0.35); z-index: 99999; font-family: 'Segoe UI', Arial, sans-serif; display: none; flex-direction: column; overflow: hidden; } .mvs-panel-header { padding: 10px 15px; /* Reduced padding */ background-color: var(--mvs-bg-header-footer); /* No border-bottom needed if panel body is gone and footer is next */ display: flex; flex-direction: column; gap: 8px; /* Reduced gap */ align-items: flex-start; } .mvs-panel-header-top { display: flex; justify-content: space-between; align-items: center; width: 100%; } .mvs-panel-header h3 { margin: 0; font-size: 15px; font-weight: 600; color: var(--mvs-text-primary); } .mvs-panel-close-btn { background: none; border: none; color: var(--mvs-text-secondary); cursor: pointer; padding: 5px; line-height: 1; border-radius: 50%; transition: background-color 0.15s, color 0.15s; } .mvs-panel-close-btn:hover { color: var(--mvs-text-primary); background-color: var(--mvs-bg-hover); } .mvs-panel-close-btn svg { width: 18px; /* Smaller close icon */ height: 18px; stroke: currentColor; } #mvs-primary-sort-select { width: 100%; padding: 8px 10px; /* Reduced padding */ background-color: var(--mvs-bg-element); color: var(--mvs-text-primary); border: 1px solid var(--mvs-border-medium); border-radius: 5px; /* Reduced radius */ font-size: 13px; /* Reduced font size */ -webkit-appearance: none; -moz-appearance: none; appearance: none; background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23E0E0E0%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.4-5.4-13z%22%2F%3E%3C%2Fsvg%3E'); background-repeat: no-repeat; background-position: right 8px center; background-size: 10px 10px; padding-right: 30px; transition: border-color 0.15s, background-color 0.15s; } #mvs-primary-sort-select:hover { border-color: var(--mvs-border-strong); } #mvs-primary-sort-select option { background-color: var(--mvs-bg-element); color: var(--mvs-text-primary); font-size: 13px; /* Ensure option font size matches select */ } #mvs-primary-sort-select optgroup { background-color: var(--mvs-bg-header-footer); color: var(--mvs-text-secondary); font-style: italic; font-weight: normal; } #mvs-primary-sort-select optgroup option { background-color: var(--mvs-bg-element); color: var(--mvs-text-primary); font-style: normal; } .mvs-panel-body { display: none; /* Hide panel body as it's no longer used */ padding: 0; } .mvs-sort-direction-controls { /* This is inside .mvs-panel-header in HTML */ display: flex; align-items: center; margin-left: 0; margin-top: 4px; /* Reduced top margin */ background-color: var(--mvs-bg-element); border-radius: 5px; /* Reduced radius */ padding: 3px; /* Reduced padding */ border: 1px solid var(--mvs-border-medium); } .mvs-dir-btn { flex-grow: 1; background: transparent; border: none; color: var(--mvs-text-secondary); padding: 5px; /* Reduced padding */ margin: 0 1px; /* Reduced margin */ border-radius: 3px; /* Reduced radius */ cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.15s, color 0.15s; } .mvs-dir-btn:hover { color: var(--mvs-text-primary); background-color: var(--mvs-bg-hover); } .mvs-dir-btn.mvs-active-dir { background-color: var(--mvs-accent-primary); color: var(--mvs-text-primary); } .mvs-dir-btn svg { width: 16px; /* Reduced icon size */ height: 16px; stroke: currentColor; } .mvs-panel-footer { padding: 10px 15px; /* Reduced padding */ background-color: var(--mvs-bg-header-footer); border-top: 1px solid var(--mvs-border-medium); /* Keep top border for separation */ display: flex; justify-content: space-between; /* Space out buttons for compact footer */ gap: 8px; } .mvs-panel-action-btn { padding: 8px 12px; /* Reduced padding */ flex-grow: 1; /* Make buttons take equal space in footer */ text-align: center; /* Center text in button */ border-radius: 5px; /* Reduced radius */ font-size: 12px; /* Reduced font size */ font-weight: 600; cursor: pointer; border: none; color: var(--mvs-text-primary); transition: background-color 0.2s, transform 0.1s; } .mvs-panel-action-btn:active { transform: scale(0.98); } .mvs-panel-action-btn.mvs-apply-btn { background-color: var(--mvs-accent-primary); } .mvs-panel-action-btn.mvs-apply-btn:hover { background-color: var(--mvs-accent-hover); } .mvs-panel-action-btn.mvs-clear-btn { background-color: var(--mvs-bg-element); } .mvs-panel-action-btn.mvs-clear-btn:hover { background-color: var(--mvs-bg-hover); } #loading-indicator { background-color: rgba(30, 30, 30, 0.9) !important; color: var(--mvs-text-primary) !important; padding: 25px 30px !important; border-radius: 8px !important; border: 1px solid var(--mvs-border-medium) !important; box-shadow: 0 4px 15px rgba(0,0,0,0.5) !important; font-family: 'Segoe UI', Arial, sans-serif !important; font-size: 18px !important; font-weight: 500 !important; } #pagination-controls { background-color: var(--mvs-bg-header-footer) !important; color: var(--mvs-text-primary) !important; padding: 10px 15px !important; border: 1px solid var(--mvs-border-medium) !important; border-radius: 6px !important; box-shadow: 0 -2px 8px rgba(0,0,0,0.3) !important; font-family: 'Segoe UI', Arial, sans-serif !important; font-size: 14px !important; bottom: 20px !important; } #pagination-controls button { background-color: var(--mvs-bg-element); color: var(--mvs-text-primary); border: 1px solid var(--mvs-border-medium); padding: 7px 14px; margin: 0 5px; cursor: pointer; border-radius: 5px; font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; font-weight: 500; transition: background-color 0.15s, border-color 0.15s; } #pagination-controls button:hover:not(:disabled) { background-color: var(--mvs-bg-hover); border-color: var(--mvs-border-strong); } #pagination-controls button:disabled { background-color: var(--mvs-bg-header-footer); color: var(--mvs-text-disabled); cursor: default; border-color: var(--mvs-border-light); } #pagination-controls span { color: var(--mvs-text-primary); margin: 0 10px; font-family: 'Segoe UI', Arial, sans-serif; font-size: 14px; } `; function addGlobalStyle(css) { var head, style; head = document.getElementsByTagName('head')[0]; if (!head) { return; } style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = css; head.appendChild(style); } // --- Sorting Logic (largely unchanged) --- function nullSafeSort(valA, valB, ascending = true, aIsBetter = false) { const factor = ascending ? 1 : -1; const aIsNull = valA === null || valA === undefined; const bIsNull = valB === null || valB === undefined; if (aIsNull && bIsNull) return 0; if (aIsNull) return aIsBetter ? -1 * factor : 1 * factor; if (bIsNull) return aIsBetter ? 1 * factor : -1 * factor; if (valA < valB) return -1 * factor; if (valA > valB) return 1 * factor; return 0; } const Sorters = { 'titleAZ': (a, b) => (a.title || '').localeCompare(b.title || ''), 'titleZA': (a, b) => (b.title || '').localeCompare(a.title || ''), 'viewsMost': (a, b) => nullSafeSort(a.views, b.views, false), 'viewsLeast': (a, b) => nullSafeSort(a.views, b.views, true), 'durationLongest': (a, b) => nullSafeSort(a.duration, b.duration, false), 'durationShortest': (a, b) => nullSafeSort(a.duration, b.duration, true), 'uploaderAZ': (a, b) => (a.uploader || '').localeCompare(b.uploader || ''), 'uploaderZA': (a, b) => (b.uploader || '').localeCompare(a.uploader || ''), 'deep:uploadDateNewest': (a, b) => nullSafeSort(a.uploadDate ? a.uploadDate.getTime() : null, b.uploadDate ? b.uploadDate.getTime() : null, false), 'deep:uploadDateOldest': (a, b) => nullSafeSort(a.uploadDate ? a.uploadDate.getTime() : null, b.uploadDate ? b.uploadDate.getTime() : null, true), 'deep:favoritesMost': (a, b) => nullSafeSort(a.favorites, b.favorites, false), 'deep:favoritesLeast': (a, b) => nullSafeSort(a.favorites, b.favorites, true), 'deep:commentsMost': (a, b) => nullSafeSort(a.commentsCount, b.commentsCount, false), 'deep:commentsLeast': (a, b) => nullSafeSort(a.commentsCount, b.commentsCount, true), 'deep:resolutionHighest': (a, b) => nullSafeSort(a.resolution, b.resolution, false), 'deep:resolutionLowest': (a, b) => nullSafeSort(a.resolution, b.resolution, true), 'deep:engagementCommentsMost': (a,b) => { const valA = (a.views > 0 && a.commentsCount !== null) ? a.commentsCount / a.views : null; const valB = (b.views > 0 && b.commentsCount !== null) ? b.commentsCount / b.views : null; return nullSafeSort(valA, valB, false); }, 'deep:engagementCommentsLeast': (a,b) => { const valA = (a.views > 0 && a.commentsCount !== null) ? a.commentsCount / a.views : null; const valB = (b.views > 0 && b.commentsCount !== null) ? b.commentsCount / b.views : null; return nullSafeSort(valA, valB, true); }, 'deep:engagementFavoritesMost': (a,b) => { const valA = (a.views > 0 && a.favorites !== null) ? a.favorites / a.views : null; const valB = (b.views > 0 && b.favorites !== null) ? b.favorites / b.views : null; return nullSafeSort(valA, valB, false); }, 'deep:engagementFavoritesLeast': (a,b) => { const valA = (a.views > 0 && a.favorites !== null) ? a.favorites / a.views : null; const valB = (b.views > 0 && b.favorites !== null) ? b.favorites / b.views : null; return nullSafeSort(valA, valB, true); }, }; function applySorting(dataArray, sortKey) { if (sortKey === 'random') { shuffleArray(dataArray); } else if (Sorters[sortKey]) { dataArray.sort(Sorters[sortKey]); } else if (sortKey === 'NO_SORT') { // Do nothing, keep original order (assuming dataArray is already in original order) } else { console.warn(`Unknown sort key: ${sortKey}. Defaulting to viewsMost.`); dataArray.sort(Sorters['viewsMost']); } } // --- UI & Pagination (largely unchanged logic, but invocation changes) --- function displayPage($container, videoData, page, videosPerPage) { $container.empty(); const start = (page - 1) * videosPerPage; const end = Math.min(start + videosPerPage, videoData.length); for (let i = start; i < end; i++) { if (videoData[i] && videoData[i].element) { $container.append(videoData[i].element); } } updatePaginationControls($container, videoData, page, videosPerPage); } function updatePaginationControls($container, videoData, currentPage, videosPerPage) { const totalVideos = videoData.length; const totalPages = Math.ceil(totalVideos / videosPerPage); let $pagination = $('#pagination-controls'); if ($pagination.length) $pagination.remove(); $pagination = $('<div id="pagination-controls"></div>').css({ position: 'fixed', left: '50%', transform: 'translateX(-50%)', zIndex: 9999, textAlign: 'center' }); if (totalPages <= 1) { if (totalVideos > 0) $pagination.text(`Total videos: ${totalVideos}`); else $pagination.text(''); $('body').append($pagination); return; } const $prev = $('<button>Previous</button>'); const $next = $('<button>Next</button>'); const $pageInfo = $('<span></span>').text(` Page ${currentPage} of ${totalPages} (${totalVideos} videos) `); if (currentPage === 1) $prev.prop('disabled', true); if (currentPage === totalPages) $next.prop('disabled', true); $prev.click(() => displayPage($container, videoData, currentPage - 1, videosPerPage)); $next.click(() => displayPage($container, videoData, currentPage + 1, videosPerPage)); $pagination.append($prev, $pageInfo, $next); $('body').append($pagination); } // --- Data Extraction (unchanged) --- function extractVideoData($videoElement) { const title = ($videoElement.find('a.caption.title').text() || '').trim(); const uploader = ($videoElement.find('a.uploader').text() || '').trim(); const viewsText = ($videoElement.find('span.hits span.value').text() || '0').trim(); const durationText = ($videoElement.find('a.img-container span.size').text() || '00:00').trim(); const pageUrlAnchor = $videoElement.find('a.img-container[href], a.caption.title[href]').first(); let pageUrl = pageUrlAnchor.attr('href'); if (pageUrl && !pageUrl.startsWith('http')) { try { pageUrl = new URL(pageUrl, window.location.origin).href; } catch (e) { console.error("Error constructing absolute URL:", pageUrl, e); pageUrl = null; } } return { element: $videoElement.clone(), title: title, uploader: uploader, views: parseViews(viewsText), duration: parseDurationToSeconds(durationText), pageUrl: pageUrl, uploadDate: null, favorites: null, commentsCount: null, resolution: null, deepDataFetched: false, deepDataError: false, originalIndex: -1 // For potential revert }; } let initialVideoDataCache = []; // Cache for "Clear Sort" to revert to original page 1 // Promisified GM_xmlhttpRequest (unchanged) function GM_fetch(details) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...details, onload: function(response) { if (response.status >= 200 && response.status < 300) resolve(response); else reject(new Error(`Request failed: ${response.status} ${response.statusText} for ${details.url}`)); }, onerror: function(response) { reject(new Error(`Request error: ${response.statusText} for ${details.url}`)); }, ontimeout: function() { reject(new Error(`Request timed out for ${details.url}`)); } }); }); } // fetchDeepVideoDetails and related orchestration (unchanged logic) async function fetchDeepVideoDetails(videoObject) { if (!videoObject.pageUrl) { videoObject.deepDataError = true; return Promise.reject("No pageUrl"); } try { const response = await GM_fetch({ method: "GET", url: videoObject.pageUrl, timeout: 15000 }); const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); const dateStr = $(doc).find('#media-info .media-meta-stats span.count:nth-of-type(3)').text(); const favStr = $(doc).find('#media-info .media-meta-stats span.count:nth-of-type(2)').text(); const commentStr = $(doc).find('#media-comments .comments-count span.count').text(); const videoElem = $(doc).find('video[id^="ml-video-"]').first(); const qualityStr = videoElem.length ? videoElem.attr('data-quality') : null; videoObject.uploadDate = parseUploadDate(dateStr); videoObject.favorites = parseFavorites(favStr); videoObject.commentsCount = parseComments(commentStr); videoObject.resolution = parseResolution(qualityStr); videoObject.deepDataFetched = true; videoObject.deepDataError = false; } catch (error) { console.error(`Failed to fetch or parse deep data for ${videoObject.pageUrl}:`, error); videoObject.deepDataError = true; throw error; } } let activeDeepFetches = 0; let deepFetchQueue = []; let allVideoDataForSort = []; // Renamed for clarity, holds all videos after gallery fetch let currentSortKeyBeingApplied = ''; let currentContainerForDisplay = null; let currentLoadingIndicator = null; function updateDeepFetchProgress(processedCount, totalToFetch) { if (currentLoadingIndicator && currentLoadingIndicator.length) { currentLoadingIndicator.text(`Fetching video details: ${processedCount}/${totalToFetch}...`); } } function processNextInDeepFetchQueue() { if (deepFetchQueue.length === 0 && activeDeepFetches === 0) { if (currentLoadingIndicator) currentLoadingIndicator.text('Sorting...'); applySorting(allVideoDataForSort, currentSortKeyBeingApplied); if (currentLoadingIndicator) currentLoadingIndicator.remove(); currentLoadingIndicator = null; if (currentContainerForDisplay) { displayPage(currentContainerForDisplay, allVideoDataForSort, 1, VIDEOS_PER_PAGE_DISPLAY); } return; } while (activeDeepFetches < MAX_CONCURRENT_DEEP_FETCHES && deepFetchQueue.length > 0) { activeDeepFetches++; const { video, totalToFetch } = deepFetchQueue.shift(); fetchDeepVideoDetails(video) .catch(err => { /* Error handled */ }) .finally(() => { activeDeepFetches--; const processedCount = totalToFetch - deepFetchQueue.length - activeDeepFetches; updateDeepFetchProgress(processedCount, totalToFetch); processNextInDeepFetchQueue(); }); } } function initiateDeepSortProcess(sortKeyToApply) { const itemsToFetch = allVideoDataForSort.filter(v => !v.deepDataFetched && !v.deepDataError && v.pageUrl); if (itemsToFetch.length === 0) { if (currentLoadingIndicator) currentLoadingIndicator.text('Sorting...'); applySorting(allVideoDataForSort, sortKeyToApply); if (currentLoadingIndicator) currentLoadingIndicator.remove(); currentLoadingIndicator = null; displayPage(currentContainerForDisplay, allVideoDataForSort, 1, VIDEOS_PER_PAGE_DISPLAY); return; } deepFetchQueue = itemsToFetch.map(video => ({ video: video, totalToFetch: itemsToFetch.length })); updateDeepFetchProgress(0, itemsToFetch.length); processNextInDeepFetchQueue(); } // --- Main Sorting and Pagination Process --- function prepareAndSortGallery($container, $initialVideosOnPage, sortOptionKey) { currentSortKeyBeingApplied = sortOptionKey; currentContainerForDisplay = $container; if (currentLoadingIndicator && currentLoadingIndicator.length) currentLoadingIndicator.remove(); currentLoadingIndicator = $('<div id="loading-indicator">Loading page 1...</div>').css({ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', zIndex: 10000 }); $('body').append(currentLoadingIndicator); if (allVideoDataForSort.length > 0 && !sortOptionKey.startsWith('deep:')) { console.log('Using cached gallery data. Total videos:', allVideoDataForSort.length); currentLoadingIndicator.text('Sorting...'); applySorting(allVideoDataForSort, currentSortKeyBeingApplied); currentLoadingIndicator.remove(); currentLoadingIndicator = null; displayPage(currentContainerForDisplay, allVideoDataForSort, 1, VIDEOS_PER_PAGE_DISPLAY); return; } if (allVideoDataForSort.length > 0 && sortOptionKey.startsWith('deep:')) { console.log('Using cached gallery data, initiating deep sort. Total videos:', allVideoDataForSort.length); initiateDeepSortProcess(currentSortKeyBeingApplied); return; } allVideoDataForSort = []; $initialVideosOnPage.each(function(index) { const videoEntry = extractVideoData($(this)); videoEntry.originalIndex = index; allVideoDataForSort.push(videoEntry); }); initialVideoDataCache = [...allVideoDataForSort]; console.log(`Initial videos processed: ${allVideoDataForSort.length}`); let $pageLinks = $('a[href*="page="], .page, .page-item').filter(function() { return $(this).text().match(/^\d+$/) || $(this).attr('href')?.match(/page=(\d+)/); }); let totalSitePages = 1; if ($pageLinks.length) { let maxPage = 0; $pageLinks.each(function() { let pageNumText = $(this).text().trim(); let pageNumHref = $(this).attr('href'); let pageNum = parseInt(pageNumText, 10); if (isNaN(pageNum) && pageNumHref) { const match = pageNumHref.match(/page=(\d+)/); if (match && match[1]) pageNum = parseInt(match[1], 10); } if (!isNaN(pageNum) && pageNum > maxPage) maxPage = pageNum; }); totalSitePages = Math.max(1, maxPage); } console.log('Total site pages detected:', totalSitePages); function completeGalleryFetchAndProceed() { console.log('All gallery pages fetched. Total videos:', allVideoDataForSort.length); if (currentSortKeyBeingApplied.startsWith('deep:')) { initiateDeepSortProcess(currentSortKeyBeingApplied); } else { currentLoadingIndicator.text('Sorting...'); applySorting(allVideoDataForSort, currentSortKeyBeingApplied); currentLoadingIndicator.remove(); currentLoadingIndicator = null; displayPage(currentContainerForDisplay, allVideoDataForSort, 1, VIDEOS_PER_PAGE_DISPLAY); } } if (totalSitePages <= 1) { completeGalleryFetchAndProceed(); return; } let baseUrl = window.location.href.split('?')[0].split('#')[0]; let pagesFetchedSuccessfully = 1; function fetchPage(page) { if (page > totalSitePages) { completeGalleryFetchAndProceed(); return; } currentLoadingIndicator.text(`Loading gallery page ${page} of ${totalSitePages}...`); $.get(`${baseUrl}?page=${page}`, function(data) { let $pageContent = $(data); let $videosOnFetchedPage = $pageContent.find(VIDEO_ITEM_SELECTOR); $videosOnFetchedPage.each(function() { const videoEntry = extractVideoData($(this)); allVideoDataForSort.push(videoEntry); }); pagesFetchedSuccessfully++; fetchPage(page + 1); }).fail(function() { console.log('Failed to fetch gallery page:', page, ". Skipping."); fetchPage(page + 1); }); } fetchPage(2); } function waitForVideos(callback) { let attempts = 0; const maxAttempts = 20; const interval = setInterval(function() { let $videos = $(VIDEO_ITEM_SELECTOR); if ($videos.length > 0) { clearInterval(interval); callback($videos); } else { attempts++; if (attempts >= maxAttempts) { clearInterval(interval); console.log('Max attempts reached. No videos found.'); callback($([])); } } }, 500); } // --- New UI Panel Logic --- let panelSelectedSortId = DEFAULT_SORT_ID; let panelSelectedSortDirection = DEFAULT_SORT_DIR; // 'asc', 'desc', or null for 'random' function updateSortPanelUI() { // Update dropdown selection $('#mvs-primary-sort-select').val(panelSelectedSortId); // Update direction buttons const $dirControls = $('#mvs-sort-direction-controls-global'); $dirControls.find('.mvs-dir-btn').removeClass('mvs-active-dir'); const selectedSortDef = SORT_DEFINITIONS.find(s => s.id === panelSelectedSortId); if (selectedSortDef && !selectedSortDef.noDirection) { $dirControls.show(); if (panelSelectedSortDirection === 'asc') { $dirControls.find('.mvs-dir-asc').addClass('mvs-active-dir'); } else if (panelSelectedSortDirection === 'desc') { $dirControls.find('.mvs-dir-desc').addClass('mvs-active-dir'); } } else { $dirControls.hide(); // Hide direction buttons for random sort } // Update active state of sort items (if they were still individual items) // In this new design, individual sort items are not clickable for selection, // so this part is no longer directly relevant for selection highlighting. // However, if we want to visually indicate the selected item in the list, // we would need to iterate through them and apply a class. // For now, the dropdown is the primary selection indicator. } $(document).ready(function() { addGlobalStyle(newStyles); waitForVideos(function($initialVideos) { let $videoGalleryContainer = $initialVideos.first().parent(); if ($initialVideos.parent('.thumbs, .media-list, .video-list, #discover-grid-wrapper div.row, .thumbs.uploads').length > 0) { $videoGalleryContainer = $initialVideos.parent('.thumbs, .media-list, .video-list, #discover-grid-wrapper div.row, .thumbs.uploads').first(); } else if ($initialVideos.closest('.thumbs, .media-list, .video-list, #discover-grid-wrapper div.row, .thumbs.uploads').length > 0) { $videoGalleryContainer = $initialVideos.closest('.thumbs, .media-list, .video-list, #discover-grid-wrapper div.row, .thumbs.uploads').first(); } if ($initialVideos.length === 0 || $videoGalleryContainer.length === 0) { console.log('No video gallery detected. Sorter UI not added.'); return; } // --- Create Main Toggle Button --- const $sortToggleButton = $(` <button id="mvs-sort-toggle-btn" title="Sort Options"> ${SVG_ICONS.filter} </button> `); $('body').append($sortToggleButton); // --- Create Sort Panel (initially hidden) --- const $sortPanelContainer = $(` <div id="mvs-sort-panel-container"> <div class="mvs-panel-header"> <div class="mvs-panel-header-top"> <h3>Sort Videos</h3> <button class="mvs-panel-close-btn" title="Close">${SVG_ICONS.close}</button> </div> <select id="mvs-primary-sort-select"></select> <div id="mvs-sort-direction-controls-global" class="mvs-sort-direction-controls"> <button class="mvs-dir-btn mvs-dir-asc" title="Sort Ascending" data-dir="asc">${SVG_ICONS.arrowUp}</button> <button class="mvs-dir-btn mvs-dir-desc" title="Sort Descending" data-dir="desc">${SVG_ICONS.arrowDown}</button> </div> </div> <div class="mvs-panel-body"></div> <div class="mvs-panel-footer"> <button class="mvs-panel-action-btn mvs-clear-btn">Clear Sort</button> <button class="mvs-panel-action-btn mvs-apply-btn">Apply Sort</button> </div> </div> `); const $panelBody = $sortPanelContainer.find('.mvs-panel-body'); const $primarySortSelect = $sortPanelContainer.find('#mvs-primary-sort-select'); // Populate primary sort dropdown const iconMap = { title: '📝', views: '👁️', duration: '⏱️', uploader: '👤', uploadDate: '📅', favorites: '⭐', comments: '💬', resolution: '📺', engagementComments: '📈', engagementFavorites: '💖', random: '🎲' }; const groupedSortsForDropdown = SORT_DEFINITIONS.reduce((acc, sortDef) => { if (!sortDef.noDirection) { // Only add sortable items to dropdown acc[sortDef.group] = acc[sortDef.group] || []; acc[sortDef.group].push(sortDef); } else if (sortDef.id === 'random') { // Add random as a special case acc[sortDef.group] = acc[sortDef.group] || []; acc[sortDef.group].push(sortDef); } return acc; }, {}); for (const groupName in groupedSortsForDropdown) { const $optgroup = $(`<optgroup label="${groupName}"></optgroup>`); groupedSortsForDropdown[groupName].forEach(sortDef => { const icon = iconMap[sortDef.id] || '▫️'; // Default icon if not found $optgroup.append(`<option value="${sortDef.id}">${icon} ${sortDef.label}</option>`); }); $primarySortSelect.append($optgroup); } // Set initial selection for dropdown $primarySortSelect.val(DEFAULT_SORT_ID); // The detailed list of sort types in the panel body is removed as per user feedback. // $panelBody will remain empty or can be hidden via CSS. $('body').append($sortPanelContainer); // --- Event Handlers --- $sortToggleButton.on('click', () => { $sortPanelContainer.fadeToggle(200); $sortToggleButton.toggle(); // Hide toggle button when panel opens updateSortPanelUI(); // Update UI when panel opens }); const hidePanel = () => { $sortPanelContainer.fadeOut(200, () => { $sortToggleButton.show(); // Show toggle button when panel closes }); }; $sortPanelContainer.find('.mvs-panel-close-btn').on('click', hidePanel); $primarySortSelect.on('change', function() { panelSelectedSortId = $(this).val(); const selectedSortDef = SORT_DEFINITIONS.find(s => s.id === panelSelectedSortId); if (selectedSortDef && !selectedSortDef.noDirection) { panelSelectedSortDirection = selectedSortDef.defaultDir; } else { panelSelectedSortDirection = null; // For random or non-directional sorts } updateSortPanelUI(); }); $('#mvs-sort-direction-controls-global .mvs-dir-btn').on('click', function() { panelSelectedSortDirection = $(this).data('dir'); updateSortPanelUI(); }); $sortPanelContainer.find('.mvs-apply-btn').on('click', function() { const activeSortDef = SORT_DEFINITIONS.find(s => s.id === panelSelectedSortId); if (!activeSortDef) return; let sortKeyToApply; if (activeSortDef.noDirection) { sortKeyToApply = activeSortDef.key; } else { sortKeyToApply = activeSortDef.keys[panelSelectedSortDirection]; } if (sortKeyToApply) { prepareAndSortGallery($videoGalleryContainer, $(VIDEO_ITEM_SELECTOR), sortKeyToApply); } hidePanel(); // Hide panel after applying sort }); $sortPanelContainer.find('.mvs-clear-btn').on('click', function() { panelSelectedSortId = DEFAULT_SORT_ID; // Revert to default panelSelectedSortDirection = DEFAULT_SORT_DIR; updateSortPanelUI(); // For now, just resets panel. User must click "Apply Sort". }); // Initialize panel UI updateSortPanelUI(); console.log("Advanced Sorter Panel UI added."); }); }); })(jQuery);