Advanced Media Analyzer

Finds all images/videos on a page (including CSS backgrounds and embedded media) and displays them in a feature-rich, draggable panel with advanced filtering, sorting, and settings.

// ==UserScript==
// @name         Advanced Media Analyzer
// @namespace    http://tampermonkey.net/
// @version      3.4
// @description  Finds all images/videos on a page (including CSS backgrounds and embedded media) and displays them in a feature-rich, draggable panel with advanced filtering, sorting, and settings.
// @author       Gemini
// @match        *://*/*
// @grant        GM_addStyle
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// ==/UserScript==

(function() {
    'use strict';

    let allMediaData = []; // Holds all found media to allow re-sorting/filtering
    let originalElements = new Map(); // Maps a media src to its original DOM element for highlighting
    const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.ogv', '.m4v', '.mpeg'];

    // --- Settings Configuration ---
    let settings = {
        showToolbar: true,
        showBulkActions: true,
        enableHoverHighlight: true,
        scanCssBackgrounds: true
    };

    const SETTINGS_KEY = 'mediaAnalyzerSettings';

    function saveSettings() {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
    }

    function loadSettings() {
        const saved = localStorage.getItem(SETTINGS_KEY);
        if (saved) {
            settings = { ...settings, ...JSON.parse(saved) };
        }
    }
    // --- End of Settings ---


    /**
     * Initializes and injects the floating "Analyze Media" button onto the page.
     */
    function initFloatingButton() {
        const button = document.createElement('button');
        button.id = 'media-sorter-trigger-button';
        button.innerHTML = `
            <svg style="width:18px;height:18px;margin-right:8px;" viewBox="0 0 24 24">
                <path fill="currentColor" d="M10 12L12 14L14 12L10 12M10 8L12 10L14 8L10 8M10 16L12 18L14 16L10 16M21 3H3C1.9 3 1 3.9 1 5V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V5C23 3.9 22.1 3 21 3M21 19H3V5H21V19Z" />
            </svg>
            Analyze Media
        `;
        button.addEventListener('click', createSortedMediaPanel);
        document.body.appendChild(button);
    }

    /**
     * Formats bytes into a human-readable string.
     */
    function formatBytes(bytes, decimals = 2) {
        if (!bytes || bytes === 0) return '0 Bytes';
        const k = 1024;
        const dm = decimals < 0 ? 0 : decimals;
        const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
    }

    /**
     * Parses a file size string (e.g., "1.5 MB") into bytes.
     */
    function parseSizeString(sizeStr) {
        if (!sizeStr) return null;
        const cleanedStr = sizeStr.trim().toUpperCase();
        const parts = cleanedStr.match(/^([\d.]+)\s*([KMGT]?B)$/);
        if (!parts) return null;
        const value = parseFloat(parts[1]);
        const unit = parts[2];
        let multiplier = 1;
        switch (unit) {
            case 'KB': multiplier = 1024; break;
            case 'MB': multiplier = 1024 ** 2; break;
            case 'GB': multiplier = 1024 ** 3; break;
            case 'TB': multiplier = 1024 ** 4; break;
        }
        return Math.round(value * multiplier);
    }

    /**
     * Determines if a media item is an image or video based on its URL.
     * This is more robust as it ignores the element type (e.g. an <img> for a video thumbnail).
     */
    function getMediaType(src) {
        try {
            // Use URL object to ignore query strings/fragments when checking extension
            const lowerPath = new URL(src, document.baseURI).pathname.toLowerCase();
            for (const ext of VIDEO_EXTENSIONS) {
                if (lowerPath.endsWith(ext)) return 'video';
            }
        } catch (e) {
            // Fallback for data URIs or invalid URLs
            const lowerSrc = src.toLowerCase();
            for (const ext of VIDEO_EXTENSIONS) {
                 if (lowerSrc.includes(ext)) return 'video';
            }
        }
        return 'image';
    }


    /**
     * Builds the main UI panel and initiates the media scan.
     */
    function createSortedMediaPanel() {
        if (document.getElementById('media-sorter-panel')) {
            document.getElementById('media-sorter-panel').remove();
        }

        const panel = document.createElement('div');
        panel.id = 'media-sorter-panel';
        panel.innerHTML = `
            <div class="media-sorter-header">
                 <h2 id="media-sorter-title">Analyzing Media...</h2>
                <div>
                    <button id="media-sorter-settings-btn" class="header-icon-btn" title="Settings">
                        <svg viewBox="0 0 24 24" style="width:20px;height:20px;"><path fill="currentColor" d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.25C8.66,5.49,8.13,5.81,7.63,6.19L5.24,5.23C5.02,5.16,4.77,5.23,4.65,5.45L2.73,8.77 c-0.11,0.2-0.06,0.47,0.12,0.61l2.03,1.58C4.82,11.36,4.8,11.68,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.44 c0.04,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.44c0.59-0.24,1.12-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0.01,0.59-0.22l1.92-3.32c0.11-0.2,0.06-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
                    </button>
                    <button id="media-sorter-close" class="header-icon-btn" title="Close panel">&times;</button>
                </div>
            </div>
            <div class="media-sorter-toolbar" ${settings.showToolbar ? '' : 'style="display:none;"'}>
                <select id="media-filter-type">
                    <option value="all">All Media</option>
                    <option value="image">Images</option>
                    <option value="video">Videos</option>
                </select>
                <select id="media-sort-by">
                    <option value="size_desc">Sort by Size (Largest)</option>
                    <option value="size_asc">Sort by Size (Smallest)</option>
                    <option value="url_asc">Sort by URL (A-Z)</option>
                </select>
            </div>
            <div id="media-sorter-list"><div class="media-sorter-loader"></div></div>
             <div class="media-sorter-footer" ${settings.showBulkActions ? '' : 'style="display:none;"'}>
                <button id="copy-all-urls">Copy All URLs</button>
                <button id="download-all-urls">Download URLs as .txt</button>
            </div>
        `;
        document.body.appendChild(panel);
        makeDraggable(panel);

        panel.querySelector('#media-sorter-close').addEventListener('click', () => panel.remove());
        panel.querySelector('#media-sorter-settings-btn').addEventListener('click', createSettingsPanel);
        panel.querySelector('#media-filter-type').addEventListener('change', renderMediaList);
        panel.querySelector('#media-sort-by').addEventListener('change', renderMediaList);
        panel.querySelector('#copy-all-urls').addEventListener('click', copyAllUrls);
        panel.querySelector('#download-all-urls').addEventListener('click', downloadAllUrls);

        setTimeout(scanAndPopulateMedia, 100);
    }

     /**
     * Builds and displays the settings panel.
     */
    function createSettingsPanel() {
        if (document.getElementById('media-sorter-settings-panel')) return;

        const settingsPanel = document.createElement('div');
        settingsPanel.id = 'media-sorter-settings-panel';
        settingsPanel.innerHTML = `
            <div class="media-settings-content">
                <h3>Settings</h3>
                <label><input type="checkbox" data-setting="showToolbar" ${settings.showToolbar ? 'checked' : ''}> Show Filter/Sort Toolbar</label>
                <label><input type="checkbox" data-setting="showBulkActions" ${settings.showBulkActions ? 'checked' : ''}> Show Bulk Actions Footer</label>
                <label><input type="checkbox" data-setting="enableHoverHighlight" ${settings.enableHoverHighlight ? 'checked' : ''}> Enable Hover to Highlight</label>
                <label><input type="checkbox" data-setting="scanCssBackgrounds" ${settings.scanCssBackgrounds ? 'checked' : ''}> Scan for CSS Background Images</label>
                <button id="settings-close-btn">Close</button>
            </div>
        `;
        document.getElementById('media-sorter-panel').appendChild(settingsPanel);

        settingsPanel.querySelector('#settings-close-btn').addEventListener('click', () => settingsPanel.remove());
        settingsPanel.addEventListener('change', e => {
            if (e.target.type === 'checkbox') {
                const key = e.target.dataset.setting;
                const value = e.target.checked;
                settings[key] = value;
                saveSettings();
                // Dynamically update UI based on new setting
                const mainPanel = document.getElementById('media-sorter-panel');
                if (key === 'showToolbar') mainPanel.querySelector('.media-sorter-toolbar').style.display = value ? 'flex' : 'none';
                if (key === 'showBulkActions') mainPanel.querySelector('.media-sorter-footer').style.display = value ? 'flex' : 'none';
                if (key === 'scanCssBackgrounds') { // Offer to rescan
                    e.target.closest('label').innerHTML += ' <small>(<a href="#" id="rescan-link">Rescan page</a>)</small>';
                    document.getElementById('rescan-link').onclick = (evt) => {
                        evt.preventDefault();
                        mainPanel.querySelector('#media-sorter-list').innerHTML = '<div class="media-sorter-loader"></div>';
                        setTimeout(scanAndPopulateMedia, 100);
                        settingsPanel.remove();
                    };
                }
                renderMediaList();
            }
        });
    }

    /**
     * Scans the page for all types of media and then triggers the initial render.
     */
    function scanAndPopulateMedia() {
        allMediaData = [];
        originalElements.clear();
        const processedSources = new Set();
        const resourceMap = new Map(performance.getEntriesByType('resource').map(res => [res.name, res.encodedBodySize]));

        scanAttachmentLists(processedSources);
        scanHtmlTags(processedSources, resourceMap);
        if (settings.scanCssBackgrounds) {
            scanCssBackgrounds(processedSources, resourceMap);
        }

        renderMediaList();
    }

    /**
     * Renders the list of media items based on current filter and sort settings.
     */
    function renderMediaList() {
        const panel = document.getElementById('media-sorter-panel');
        if (!panel) return;

        const listContainer = panel.querySelector('#media-sorter-list');
        const titleElement = panel.querySelector('#media-sorter-title');
        const filterValue = panel.querySelector('#media-filter-type').value;
        const sortValue = panel.querySelector('#media-sort-by').value;

        let filteredData = allMediaData;
        if (filterValue !== 'all') {
            filteredData = allMediaData.filter(item => item.type === filterValue);
        }

        switch (sortValue) {
            case 'size_desc': filteredData.sort((a, b) => b.size - a.size); break;
            case 'size_asc': filteredData.sort((a, b) => a.size - b.size); break;
            case 'url_asc': filteredData.sort((a, b) => a.src.localeCompare(b.src)); break;
        }

        titleElement.textContent = `Media Analyzer (${filteredData.length} items)`;
        listContainer.innerHTML = '';

        if (filteredData.length > 0) {
            filteredData.forEach(item => {
                const itemDiv = document.createElement('div');
                itemDiv.className = 'media-sorter-item';
                itemDiv.dataset.src = item.src;

                const preview = item.previewElement.cloneNode(true);
                preview.style.width = '100px';
                preview.style.height = '100px';
                preview.style.objectFit = 'contain';
                if (preview.tagName === 'VIDEO') {
                   preview.muted = true;
                }


                const infoDiv = document.createElement('div');
                infoDiv.className = 'media-sorter-info';
                infoDiv.innerHTML = `
                    <p><strong>Size:</strong> ${formatBytes(item.size)}</p>
                    <div class="media-source-container">
                        <p><strong>Source:</strong> <a href="${item.src}" target="_blank" rel="noopener noreferrer" title="${item.src}">${item.src}</a></p>
                        ${item.src !== 'Embedded Data URI' ? '<button class="copy-src-button" title="Copy URL">Copy</button>' : ''}
                    </div>
                `;
                itemDiv.appendChild(preview);
                itemDiv.appendChild(infoDiv);
                listContainer.appendChild(itemDiv);

                if (settings.enableHoverHighlight && originalElements.has(item.src)) {
                    itemDiv.addEventListener('mouseenter', () => addHighlight(item.src));
                    itemDiv.addEventListener('mouseleave', () => removeHighlight(item.src));
                }
            });
        } else {
            listContainer.innerHTML = '<p class="media-sorter-empty">No media matching the current filter was found.</p>';
        }

        panel.querySelectorAll('.copy-src-button').forEach(button => {
            button.addEventListener('click', e => {
                const url = e.target.closest('.media-source-container').querySelector('a').href;
                navigator.clipboard.writeText(url).then(() => {
                    e.target.textContent = 'Copied!';
                    setTimeout(() => { e.target.textContent = 'Copy'; }, 2000);
                });
            });
        });
    }

    /**
     * Scans for media within custom attachment list structures.
     */
    function scanAttachmentLists(processedSources) {
        document.querySelectorAll('.attachmentList > li, .attachmentList-item').forEach(item => {
            const metaEl = item.querySelector('.file-meta');
            const previewLink = item.querySelector('a.file-preview[href]');

            // This is the essential check. We need a link and file metadata.
            if (previewLink && metaEl) {
                const src = new URL(previewLink.getAttribute('href'), document.baseURI).href;
                if (src && !processedSources.has(src)) {
                    const size = parseSizeString(metaEl.innerText);
                    if (size !== null) {
                        let previewElement;
                        const thumbnailEl = item.querySelector('img');
                        const mediaType = getMediaType(src);
                        const elementToHighlight = previewLink; // The link is always the best element to highlight

                        if (thumbnailEl) {
                            // If there's a thumbnail, use it for the preview.
                            previewElement = thumbnailEl;
                        } else if (mediaType === 'video') {
                            // If no thumbnail but it's a video, create a video element for the preview.
                            const videoPreview = document.createElement('video');
                            videoPreview.src = src;
                            previewElement = videoPreview;
                        } else {
                            // Fallback for non-video items without a thumbnail (e.g., zip files).
                            // We can skip these as this tool focuses on visual media.
                            return;
                        }

                        allMediaData.push({
                            previewElement: previewElement,
                            src: src,
                            size: size,
                            type: mediaType
                        });
                        originalElements.set(src, elementToHighlight);
                        processedSources.add(src);
                    }
                }
            }
        });
    }

    /**
     * Scans for standard <img> and embedded <video> tags on the page.
     */
    function scanHtmlTags(processedSources, resourceMap) {
        // Find all images not already processed in attachment lists
        document.querySelectorAll('img').forEach(el => {
            const src = el.currentSrc || el.src;
            if (src && !processedSources.has(src)) {
                let size = 0;
                let finalSrc = src;
                if (src.startsWith('data:')) {
                    const base64Data = src.split(',')[1];
                    if (base64Data) size = base64Data.length * 0.75;
                    finalSrc = 'Embedded Data URI';
                } else if (resourceMap.has(src)) {
                    size = resourceMap.get(src);
                }

                if (size > 0) {
                    allMediaData.push({ previewElement: el, src: finalSrc, size, type: getMediaType(finalSrc) });
                    if (finalSrc !== 'Embedded Data URI') {
                        originalElements.set(finalSrc, el);
                    }
                }
                processedSources.add(src);
            }
        });

        // Find all videos, which might be embedded differently
        document.querySelectorAll('video').forEach(el => {
            const sourceEl = el.querySelector('source');
            const src = el.src || (sourceEl ? sourceEl.src : null);
            const fullSrc = src ? new URL(src, document.baseURI).href : null;

            if (fullSrc && !processedSources.has(fullSrc)) {
                const size = resourceMap.get(fullSrc) || 0;
                // We list videos even if size isn't in resourceMap, as they are important
                 allMediaData.push({ previewElement: el, src: fullSrc, size, type: 'video' });
                 originalElements.set(fullSrc, el); // Highlight the video player itself
                 processedSources.add(fullSrc);
            }
        });
    }

    /**
     * Scans CSS rules for background-image properties.
     */
    function scanCssBackgrounds(processedSources, resourceMap) {
        const urlRegex = /url\(['"]?(.*?)['"]?\)/;
        for (const sheet of document.styleSheets) {
            try {
                if (sheet.cssRules) {
                    for (const rule of sheet.cssRules) {
                        if (rule.style && rule.style.backgroundImage && rule.style.backgroundImage.includes('url')) {
                             const matches = rule.style.backgroundImage.matchAll(new RegExp(urlRegex.source, 'g'));
                             for (const match of matches) {
                                if (match && match[1]) {
                                    const url = new URL(match[1], document.baseURI).href;
                                    if (url && !processedSources.has(url) && resourceMap.has(url)) {
                                        const size = resourceMap.get(url);
                                        if (size > 0) {
                                            const img = new Image();
                                            img.src = url;
                                            allMediaData.push({ previewElement: img, src: url, size, type: getMediaType(url) });
                                            processedSources.add(url);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (e) {
                // Ignore cross-origin stylesheet errors
            }
        }
    }


    /**
     * Highlights the corresponding element on the page.
     */
    function addHighlight(src) {
        const el = originalElements.get(src);
        if (el && typeof el.getBoundingClientRect === 'function') {
            el.scrollIntoView({ behavior: 'smooth', block: 'center' });
            el.classList.add('media-sorter-highlight');
        }
    }

    /**
     * Removes the highlight from the element on the page.
     */
    function removeHighlight(src) {
        const el = originalElements.get(src);
        if (el && typeof el.getBoundingClientRect === 'function') {
            el.classList.remove('media-sorter-highlight');
        }
    }

    /**
     * Copies all currently filtered URLs to the clipboard.
     */
    function copyAllUrls() {
        const panel = document.getElementById('media-sorter-panel');
        const button = document.getElementById('copy-all-urls');
        if (!panel || !button) return;

        const urls = Array.from(panel.querySelectorAll('.media-sorter-item a'))
            .map(a => a.href)
            .join('\n');

        if (urls) {
            navigator.clipboard.writeText(urls).then(() => {
                button.textContent = 'Copied!';
                setTimeout(() => { button.textContent = 'Copy All URLs'; }, 2000);
            });
        }
    }

    /**
     * Downloads all currently filtered URLs as a text file.
     */
    function downloadAllUrls() {
        const panel = document.getElementById('media-sorter-panel');
        if (!panel) return;

        const urls = Array.from(panel.querySelectorAll('.media-sorter-item a'))
            .map(a => a.href)
            .join('\n');

        if (urls) {
            const blob = new Blob([urls], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `media_urls_${document.domain}.txt`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
    }

    /**
     * Makes the panel draggable.
     */
    function makeDraggable(panel) {
        const header = panel.querySelector('.media-sorter-header');
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        header.onmousedown = (e) => {
            // prevent dragging when clicking on a button
            if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        };
        function elementDrag(e) {
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            panel.style.top = (panel.offsetTop - pos2) + "px";
            panel.style.left = (panel.offsetLeft - pos1) + "px";
        }
        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    /**
     * Injects all CSS styles.
     */
    function addPanelStyles() {
        GM_addStyle(`
            #media-sorter-trigger-button {
                position: fixed; bottom: 20px; right: 20px; z-index: 999998;
                background-color: #007bff; color: white; border: none;
                border-radius: 8px; padding: 10px 15px; font-size: 14px; font-weight: 500;
                cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                transition: all 0.2s ease-in-out; display: flex; align-items: center;
            }
            #media-sorter-trigger-button:hover {
                transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.2); background-color: #0069d9;
            }
            #media-sorter-panel {
                position: fixed; top: 40px; right: 40px; width: 600px; max-width: 90vw; height: 85vh;
                background-color: rgba(250, 250, 250, 0.95); border: 1px solid #ccc;
                border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.2);
                z-index: 999999; display: flex; flex-direction: column;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
                color: #333; backdrop-filter: blur(10px); resize: both; overflow: hidden;
            }
            .media-sorter-header {
                display: flex; justify-content: space-between; align-items: center;
                padding: 12px 20px; border-bottom: 1px solid #ddd;
                cursor: move; user-select: none; background-color: rgba(255,255,255,0.7);
            }
            #media-sorter-title { margin: 0; font-size: 18px; font-weight: 600; flex-grow: 1; }
            .header-icon-btn { background: none; border: none; font-size: 28px; cursor: pointer; color: #888; padding: 0 5px; }
            .header-icon-btn:hover { color: #000; }
            #media-sorter-settings-btn svg { vertical-align: middle; }
            .media-sorter-toolbar {
                padding: 10px; display: flex; gap: 10px; border-bottom: 1px solid #ddd; background: #f8f9fa;
            }
            .media-sorter-toolbar select { padding: 8px; border: 1px solid #ccc; border-radius: 5px; flex-grow: 1; }
            #media-sorter-list { overflow-y: auto; padding: 10px; flex-grow: 1; }
            .media-sorter-item {
                display: flex; align-items: flex-start; border-bottom: 1px solid #eee; padding: 15px 10px; transition: background-color 0.2s;
            }
            .media-sorter-item:hover { background-color: rgba(0, 123, 255, 0.05); }
            .media-sorter-info { margin-left: 15px; flex-grow: 1; min-width: 0; }
            .media-sorter-info p { margin: 0 0 8px 0; font-size: 13px; word-break: break-all; }
            .media-sorter-info strong { color: #000; }
            .media-source-container { display: flex; align-items: center; justify-content: space-between; }
            .media-source-container a { color: #007bff; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .media-source-container a:hover { text-decoration: underline; }
            .copy-src-button {
                margin-left: 10px; padding: 3px 8px; font-size: 12px;
                border: 1px solid #ccc; background-color: #f0f0f0; border-radius: 5px; cursor: pointer;
            }
            .copy-src-button:hover { background-color: #e0e0e0; }
            .media-sorter-footer { padding: 10px; display: flex; gap: 10px; border-top: 1px solid #ddd; background: #f8f9fa; }
            .media-sorter-footer button {
                padding: 8px 12px; border: 1px solid #007bff; background-color: transparent; color: #007bff;
                border-radius: 5px; cursor: pointer; transition: all 0.2s; flex-grow: 1;
            }
            .media-sorter-footer button:hover { background-color: #007bff; color: white; }
            .media-sorter-empty, .media-sorter-loader { text-align: center; color: #777; margin-top: 40px; }
            .media-sorter-loader {
                border: 5px solid #f3f3f3; border-top: 5px solid #3498db;
                border-radius: 50%; width: 40px; height: 40px;
                animation: spin 1s linear infinite; margin: 40px auto;
            }
            @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
            .media-sorter-highlight {
                outline: 3px solid #007bff !important;
                box-shadow: 0 0 20px rgba(0, 123, 255, 0.8) !important;
                transition: outline 0.2s ease, box-shadow 0.2s ease;
            }
            #media-sorter-settings-panel {
                position: absolute; top: 0; left: 0; right: 0; bottom: 0;
                background-color: rgba(250, 250, 250, 0.98);
                z-index: 10;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .media-settings-content {
                background-color: white; padding: 25px; border-radius: 8px;
                box-shadow: 0 5px 15px rgba(0,0,0,0.1);
                display: flex; flex-direction: column; gap: 15px;
            }
            .media-settings-content h3 { margin: 0 0 10px; }
            .media-settings-content label { display: block; font-size: 14px; cursor: pointer; }
            .media-settings-content input { margin-right: 8px; }
            #settings-close-btn {
                margin-top: 10px; padding: 10px; background-color: #007bff; color: white;
                border: none; border-radius: 5px; cursor: pointer;
            }
        `);
    }

    // --- Script Initialization ---
    loadSettings();
    initFloatingButton();
    addPanelStyles();
})();