Handlers Helper (Improved)

Helper for protocol_hook.lua - Enhanced drag-to-action system for media links with MPV integration. Supports multiple protocols (mpv://, streamlink, yt-dlp) and customizable actions.

目前為 2025-09-14 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Handlers Helper (Improved)
// @namespace   Violentmonkey Scripts
// @version     4.8.7
// @description Helper for protocol_hook.lua - Enhanced drag-to-action system for media links with MPV integration. Supports multiple protocols (mpv://, streamlink, yt-dlp) and customizable actions.
// @author      hongmd (improved)
// @license     MIT
// @homepageURL https://github.com/hongmd/userscript-improved
// @supportURL  https://github.com/hongmd/userscript-improved/issues
// @include     *://*/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// @run-at      document-start
// @noframes
// ==/UserScript==

'use strict';

// === CONSTANTS AND DEFAULTS ===
const GUIDE = 'Value: pipe ytdl stream mpv iptv';

/* ACTION EXPLANATIONS:
 * 📺 pipe (UP) → mpv://mpvy/ → Pipe video to MPV with yt-dlp processing
 * 📥 ytdl (DOWN) → mpv://ytdl/ → Download video using yt-dlp  
 * 🌊 stream (LEFT) → mpv://stream/ → Stream video using streamlink
 * ▶️ mpv (RIGHT) → mpv://play/ → Direct play in MPV player
 * 📋 iptv → mpv://list/ → Add to IPTV playlist
 */
const LIVE_WINDOW_WIDTH = 400;
const LIVE_WINDOW_HEIGHT = 640;
const DRAG_THRESHOLD = 50;
const RIGHT_CLICK_DELAY = 200;

// Default values
const DEFAULTS = {
    UP: 'pipe',
    DOWN: 'ytdl',
    LEFT: 'stream',
    RIGHT: 'mpv',
    hlsdomain: 'cdn.animevui.com',
    livechat: false,
    total_direction: 4,
    down_confirm: true, // New setting to confirm DOWN action
    debug: false // Debug logging toggle
};

// Direction enum
const DirectionEnum = Object.freeze({
    CENTER: 5,
    RIGHT: 6,
    LEFT: 4,
    UP: 2,
    DOWN: 8,
    UP_LEFT: 1,
    UP_RIGHT: 3,
    DOWN_LEFT: 7,
    DOWN_RIGHT: 9
});

// === GLOBAL STATE ===
let settings = {
    UP: GM_getValue('UP', DEFAULTS.UP),
    DOWN: GM_getValue('DOWN', DEFAULTS.DOWN),
    LEFT: GM_getValue('LEFT', DEFAULTS.LEFT),
    RIGHT: GM_getValue('RIGHT', DEFAULTS.RIGHT),
    hlsdomain: GM_getValue('hlsdomain', DEFAULTS.hlsdomain),
    livechat: GM_getValue('livechat', DEFAULTS.livechat),
    total_direction: GM_getValue('total_direction', DEFAULTS.total_direction),
    down_confirm: GM_getValue('down_confirm', DEFAULTS.down_confirm),
    debug: GM_getValue('debug', DEFAULTS.debug)
};

let hlsdomainArray = settings.hlsdomain.split(',').filter(d => d.trim());
let collectedUrls = new Map(); // Use Map instead of object for better performance
let attachedElements = new WeakSet(); // Use WeakSet to prevent memory leaks

// Add CSS class for collected links styling
GM_addStyle(`
    .hh-collected-link {
        box-sizing: border-box !important;
        border: solid yellow 4px !important;
    }
`);

debugLog('Handlers Helper loaded with settings:', settings);

// === UTILITY FUNCTIONS ===
function safePrompt(message, defaultValue) {
    try {
        const result = window.prompt(message, defaultValue);
        return result === null ? null : result.trim();
    } catch (error) {
        debugError('Prompt error:', error);
        return null;
    }
}

function updateSetting(key, value) {
    settings[key] = value;
    GM_setValue(key, value);
    debugLog(`Updated ${key} to:`, value);
}

function reloadPage() {
    try {
        window.location.reload();
    } catch (error) {
        debugError('Reload failed:', error);
    }
}

// Optimized logging function - only logs when debug is enabled
function debugLog(...args) {
    if (settings.debug) {
        console.log(...args);
    }
}

function debugWarn(...args) {
    if (settings.debug) {
        console.warn(...args);
    }
}

function debugError(...args) {
    if (settings.debug) {
        console.error(...args);
    }
}

function getParentByTagName(element, tagName) {
    if (!element || typeof tagName !== 'string') return null;

    tagName = tagName.toLowerCase();
    let current = element;

    while (current && current.nodeType === Node.ELEMENT_NODE) {
        if (current.tagName && current.tagName.toLowerCase() === tagName) {
            return current;
        }
        current = current.parentNode;
    }
    return null;
}

function encodeUrl(url) {
    try {
        // Validate URL before encoding
        new URL(url);
        return btoa(url).replace(/[/+=]/g, match => 
            match === '/' ? '_' : match === '+' ? '-' : ''
        );
    } catch (error) {
        debugError('Invalid URL provided to encodeUrl:', url, error);
        return '';
    }
}

// Process DOM mutations for drag handler optimization
function processMutations(mutations) {
    let needsUpdate = false;
    mutations.forEach(function (mutation) {
        if (mutation.type === 'childList') {
            mutation.addedNodes.forEach(function (node) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    const links = node.querySelectorAll ? node.querySelectorAll('a[href]') : [];
                    if (links.length > 0) needsUpdate = true;

                    if (node.tagName === 'A' && node.href && !node.draggable) {
                        needsUpdate = true;
                    }
                }
            });
        }
    });

    if (needsUpdate) {
        makeLinksDraggable();
    }
}

// YouTube menu command helper
function addYouTubeMenuCommand(label, url, persistent) {
    GM_registerMenuCommand(label, function () {
        if (persistent) {
            if (url.includes('m.youtube.com')) {
                GM_setValue('hh_mobile', true);
            } else if (url.includes('www.youtube.com')) {
                GM_setValue('hh_mobile', false);
            }
        } else {
            GM_deleteValue('hh_mobile');
        }

        try {
            location.replace(url);
        } catch (error) {
            debugError('Failed to navigate:', error);
        }
    });
}

// === HELP AND TESTING FUNCTIONS ===
function showActionHelp() {
    const helpText = `🎮 DRAG DIRECTIONS & ACTIONS:

📺 UP (↑): ${settings.UP}
   → Pipes video to MPV with yt-dlp processing
   → Good for: YouTube, complex streams

📥 DOWN (↓): ${settings.DOWN} ${settings.down_confirm ? '(Confirm: ON)' : '(Confirm: OFF)'}
   → Downloads video using yt-dlp
   → Good for: Saving videos locally

🌊 LEFT (←): ${settings.LEFT}
   → Streams video using streamlink
   → Good for: Twitch, live streams

▶️ RIGHT (→): ${settings.RIGHT}
   → Direct play in MPV player
   → Good for: Direct video files

📋 UP-LEFT (↖): list
   → Adds to IPTV/playlist
   → Good for: Building playlists

🎯 USAGE:
1. Hover over a video link
2. Drag in desired direction
3. Release to trigger action

🔧 Settings: ${settings.total_direction} directions, HLS: ${hlsdomainArray.length} domains

🐛 TROUBLESHOOTING:
- Check browser console (F12) for debug logs
- Make sure links are draggable (script auto-enables)
- Try dragging further than ${DRAG_THRESHOLD}px
- Look for "🚀 Drag started" and "⬆️ Executing UP action" logs`;

    alert(helpText);
}

function testDirections() {
    debugLog('🧪 Testing direction detection:');
    const tests = [
        { name: 'UP', start: [100, 100], end: [100, 50] },
        { name: 'DOWN', start: [100, 100], end: [100, 150] },
        { name: 'LEFT', start: [100, 100], end: [50, 100] },
        { name: 'RIGHT', start: [100, 100], end: [150, 100] },
        { name: 'UP-LEFT', start: [100, 100], end: [50, 50] },
        { name: 'NO MOVEMENT', start: [100, 100], end: [105, 105] }
    ];

    tests.forEach(test => {
        const direction = getDirection(test.start[0], test.start[1], test.end[0], test.end[1]);
        debugLog(`${test.name}: (${test.start[0]},${test.start[1]}) -> (${test.end[0]},${test.end[1]}) = Direction ${direction}`);
    });
}

// === MENU COMMANDS SETUP ===
function setupMenuCommands() {
    // Help command first
    GM_registerMenuCommand('❓ Show Action Help', showActionHelp);
    GM_registerMenuCommand('🧪 Test Directions', testDirections);

    GM_registerMenuCommand(`📺 UP: ${settings.UP}`, function () {
        const value = safePrompt(GUIDE + '\n\n↑ UP Action (pipe = stream to MPV with yt-dlp)', settings.UP);
        if (value) {
            updateSetting('UP', value);
            reloadPage();
        }
    });

    GM_registerMenuCommand(`📥 DOWN: ${settings.DOWN}`, function () {
        const value = safePrompt(GUIDE + '\n\n↓ DOWN Action (ytdl = download with yt-dlp)', settings.DOWN);
        if (value) {
            updateSetting('DOWN', value);
            reloadPage();
        }
    });

    GM_registerMenuCommand(`🌊 LEFT: ${settings.LEFT}`, function () {
        const value = safePrompt(GUIDE + '\n\n← LEFT Action (stream = use streamlink)', settings.LEFT);
        if (value) {
            updateSetting('LEFT', value);
            reloadPage();
        }
    });

    GM_registerMenuCommand(`▶️ RIGHT: ${settings.RIGHT}`, function () {
        const value = safePrompt(GUIDE + '\n\n→ RIGHT Action (mpv = direct play)', settings.RIGHT);
        if (value) {
            updateSetting('RIGHT', value);
            reloadPage();
        }
    });

    GM_registerMenuCommand('HLS Domains', function () {
        const value = safePrompt('Example: 1.com,2.com,3.com,4.com', settings.hlsdomain);
        if (value !== null) {
            updateSetting('hlsdomain', value);
            hlsdomainArray = value.split(',').filter(d => d.trim());
        }
    });

    GM_registerMenuCommand(`Live Chat: ${settings.livechat}`, function () {
        updateSetting('livechat', !settings.livechat);
        reloadPage();
    });

    GM_registerMenuCommand(`Directions: ${settings.total_direction}`, function () {
        const newValue = settings.total_direction === 4 ? 8 : 4;
        updateSetting('total_direction', newValue);
        reloadPage();
    });

    GM_registerMenuCommand(`DOWN Confirm: ${settings.down_confirm ? 'ON' : 'OFF'}`, function () {
        updateSetting('down_confirm', !settings.down_confirm);
        reloadPage();
    });

    GM_registerMenuCommand(`🐛 Debug Mode: ${settings.debug ? 'ON' : 'OFF'}`, function () {
        updateSetting('debug', !settings.debug);
        reloadPage();
    });
}

// === LIVE CHAT FUNCTIONS ===
function openPopout(chatUrl) {
    try {
        const features = [
            'fullscreen=no',
            'toolbar=no',
            'titlebar=no',
            'menubar=no',
            'location=no',
            `width=${LIVE_WINDOW_WIDTH}`,
            `height=${LIVE_WINDOW_HEIGHT}`
        ].join(',');

        window.open(chatUrl, '', features);
    } catch (error) {
        debugError('Failed to open popout:', error);
    }
}

function openLiveChat(url) {
    try {
        const urlObj = new URL(url);
        const href = urlObj.href;

        if (href.includes('www.youtube.com/watch') || href.includes('m.youtube.com/watch')) {
            const videoId = urlObj.searchParams.get('v');
            if (videoId) {
                openPopout(`https://www.youtube.com/live_chat?is_popout=1&v=${videoId}`);
            }
        } else if (href.match(/https:\/\/.*?\.twitch\.tv\//)) {
            openPopout(`https://www.twitch.tv/popout${urlObj.pathname}/chat?popout=`);
        } else if (href.match(/https:\/\/.*?\.nimo\.tv\//)) {
            try {
                const selector = `a[href="${urlObj.pathname}"] .nimo-player.n-as-full`;
                const element = document.querySelector(selector);
                if (element && element.id) {
                    const streamId = element.id.replace('home-hot-', '');
                    openPopout(`https://www.nimo.tv/popout/chat/${streamId}`);
                }
            } catch (error) {
                debugError('Nimo.tv chat extraction failed:', error);
            }
        }
    } catch (error) {
        debugError('Live chat opener failed:', error);
    }
}

// === ACTION EXECUTION ===
function executeAction(targetUrl, actionType) {
    debugLog('Executing action:', actionType, 'for URL:', targetUrl);

    // Check if this is a DOWN action and confirmation is enabled
    if (actionType === settings.DOWN && settings.down_confirm) {
        const confirmed = confirm(`Confirm DOWN action (${actionType})?\n\nURL: ${targetUrl}\n\nClick OK to proceed or Cancel to abort.`);
        if (!confirmed) {
            debugLog('DOWN action cancelled by user');
            return;
        }
    }

    let finalUrl = '';
    let app = 'play';
    let isHls = false;

    // Check HLS domains
    for (const domain of hlsdomainArray) {
        if (domain && (targetUrl.includes(domain) || document.domain.includes(domain))) {
            if (actionType === 'stream') {
                targetUrl = targetUrl.replace(/^https?:/, 'hls:');
            }
            isHls = true;
            break;
        }
    }

    // Handle different URL types
    if (targetUrl.startsWith('http') || targetUrl.startsWith('hls:')) {
        finalUrl = targetUrl;
    } else if (targetUrl.startsWith('mpv://')) {
        try {
            location.href = targetUrl;
        } catch (error) {
            debugError('Failed to navigate to mpv URL:', error);
        }
        return;
    } else {
        finalUrl = location.href;
    }

    // Process collected URLs
    let urlString = '';
    if (collectedUrls.size > 0) {
        const urls = Array.from(collectedUrls.keys());
        urlString = urls.join(' ');

        // Reset visual indicators
        collectedUrls.forEach((element) => {
            try {
                element.classList.remove('hh-collected-link');
            } catch (error) {
                debugError('Failed to reset element class:', error);
            }
        });

        collectedUrls.clear();
        debugLog('Processed collected URLs:', urlString);
    } else {
        urlString = finalUrl;
    }

    // Determine app type and protocol action
    switch (actionType) {
        case 'pipe':
            app = 'mpvy'; // Pipe video stream to MPV with yt-dlp preprocessing
            break;
        case 'iptv':
            app = 'list'; // Add to IPTV playlist
            break;
        case 'stream':
            app = 'stream'; // Stream using streamlink
            break;
        case 'mpv':
        case 'vid':
            app = 'play'; // Direct play in MPV
            break;
        default:
            app = actionType; // Pass through custom actions
    }

    // Build final URL
    const encodedUrl = encodeUrl(urlString);
    const encodedReferer = encodeUrl(location.href);
    let protocolUrl = `mpv://${app}/${encodedUrl}/?referer=${encodedReferer}`;

    if (isHls) {
        protocolUrl += '&hls=1';
    }

    debugLog('Action details:', {
        actionType,
        app,
        finalUrl,
        urlString,
        isHls,
        protocolUrl
    });

    // Open live chat if needed
    if (actionType === 'stream' && settings.livechat) {
        openLiveChat(finalUrl);
    }

    debugLog('Final protocol URL:', protocolUrl);

    try {
        location.href = protocolUrl;
    } catch (error) {
        debugError('Failed to navigate to protocol URL:', error);
    }
}

// === DIRECTION CALCULATION ===
function getDirection(startX, startY, endX, endY) {
    const deltaX = endX - startX;
    const deltaY = endY - startY;

    debugLog('Direction calculation:', {
        start: [startX, startY],
        end: [endX, endY],
        delta: [deltaX, deltaY],
        threshold: DRAG_THRESHOLD
    });

    // Check for center (no movement)
    if (Math.abs(deltaX) < DRAG_THRESHOLD && Math.abs(deltaY) < DRAG_THRESHOLD) {
        debugLog('Direction: CENTER (no movement)');
        return DirectionEnum.CENTER;
    }

    let direction;

    if (settings.total_direction === 4) {
        // 4-direction mode
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            direction = deltaX > 0 ? DirectionEnum.RIGHT : DirectionEnum.LEFT;
        } else {
            direction = deltaY > 0 ? DirectionEnum.DOWN : DirectionEnum.UP;
        }
        debugLog('4-direction mode, result:', direction, direction === DirectionEnum.UP ? '(UP)' : direction === DirectionEnum.DOWN ? '(DOWN)' : direction === DirectionEnum.LEFT ? '(LEFT)' : '(RIGHT)');
    } else {
        // 8-direction mode
        if (deltaX === 0) {
            direction = deltaY > 0 ? DirectionEnum.DOWN : DirectionEnum.UP;
            debugLog('8-direction mode, vertical movement, result:', direction);
        } else {
            const slope = deltaY / deltaX;
            const absSlope = Math.abs(slope);

            if (absSlope < 0.4142) { // ~22.5 degrees
                direction = deltaX > 0 ? DirectionEnum.RIGHT : DirectionEnum.LEFT;
            } else if (absSlope > 2.4142) { // ~67.5 degrees
                direction = deltaY > 0 ? DirectionEnum.DOWN : DirectionEnum.UP;
            } else {
                // Diagonal directions
                if (deltaX > 0) {
                    direction = deltaY > 0 ? DirectionEnum.DOWN_RIGHT : DirectionEnum.UP_RIGHT;
                } else {
                    direction = deltaY > 0 ? DirectionEnum.DOWN_LEFT : DirectionEnum.UP_LEFT;
                }
            }
            debugLog('8-direction mode, slope:', slope, 'result:', direction);
        }
    }

    return direction;
}

// === DRAG HANDLING ===
function attachDragHandler(element) {
    if (!element || attachedElements.has(element)) return;

    attachedElements.add(element);

    // Make sure elements are draggable - optimized version
    if (element === document) {
        // Use a single observer with throttling
        let observerTimeout;
        const observer = new MutationObserver(function (mutations) {
            // Throttle observer updates to prevent excessive processing
            if (observerTimeout) return;

            observerTimeout = setTimeout(() => {
                observerTimeout = null;
                processMutations(mutations);
            }, 100); // 100ms throttle
        });

        observer.observe(document, {
            childList: true,
            subtree: true
        });

        // Initial setup
        makeLinksDraggable();

        // Use event delegation for drag events
        let dragState = null;

        document.addEventListener('dragstart', function (event) {
            const link = getDraggableLink(event.target);
            if (!link) return;

            debugLog('🚀 Drag started on element:', event.target.tagName, link.href || event.target.src);
            dragState = {
                startX: event.clientX,
                startY: event.clientY,
                target: link
            };

            // Prevent default drag behavior for non-draggable elements
            if (!event.target.draggable) {
                event.preventDefault();
                return;
            }
        }, { passive: true });

        document.addEventListener('dragend', function (event) {
            if (!dragState) return;

            debugLog('🏁 Drag ended');
            const endX = event.clientX;
            const endY = event.clientY;

            const direction = getDirection(dragState.startX, dragState.startY, endX, endY);

            debugLog(`🎯 Final drag direction: ${direction} (${dragState.startX},${dragState.startY} -> ${endX},${endY})`);
            debugLog('Current settings:', settings);

            const targetHref = dragState.target.href || dragState.target.src;
            if (!targetHref) {
                debugWarn('❌ No href or src found on target element');
                dragState = null;
                return;
            }

            debugLog('🔗 Target URL:', targetHref);

            // Execute action based on direction
            switch (direction) {
                case DirectionEnum.RIGHT:
                    debugLog('➡️ Executing RIGHT action:', settings.RIGHT);
                    executeAction(targetHref, settings.RIGHT);
                    break;
                case DirectionEnum.LEFT:
                    debugLog('⬅️ Executing LEFT action:', settings.LEFT);
                    executeAction(targetHref, settings.LEFT);
                    break;
                case DirectionEnum.UP:
                    debugLog('⬆️ Executing UP action:', settings.UP);
                    executeAction(targetHref, settings.UP);
                    break;
                case DirectionEnum.DOWN:
                    debugLog('⬇️ Executing DOWN action:', settings.DOWN);
                    executeAction(targetHref, settings.DOWN);
                    break;
                case DirectionEnum.UP_LEFT:
                    debugLog('↖️ Executing UP_LEFT action: list');
                    executeAction(targetHref, 'list');
                    break;
                default:
                    debugLog('❓ Direction not mapped to action:', direction);
            }

            dragState = null;
        }, { passive: true });
    }
}

// Helper function to find draggable link from event target
function getDraggableLink(element) {
    if (!element) return null;

    let current = element;
    while (current && current !== document) {
        if (current.tagName === 'A' && current.href) {
            return current;
        }
        current = current.parentElement;
    }
    return null;
}

// Optimized function to make links draggable
function makeLinksDraggable() {
    // Use a single query and cache the result
    const links = document.querySelectorAll('a[href]:not([draggable="true"])');
    links.forEach(link => {
        link.draggable = true;
        debugLog('Made link draggable:', link.href);
    });
}

// === RIGHT-CLICK COLLECTION ===
function setupRightClickCollection() {
    let mouseIsDown = false;
    let isHeld = false;

    document.addEventListener('mousedown', function (event) {
        const link = getParentByTagName(event.target, 'A');
        if (!link) return;

        mouseIsDown = true;

        // Cleanup listeners
        const handleMouseUp = function () {
            mouseIsDown = false;
            document.removeEventListener('mouseup', handleMouseUp);
        };

        const handleContextMenu = function (contextEvent) {
            if (isHeld) {
                contextEvent.preventDefault();
                isHeld = false;
            }
            document.removeEventListener('contextmenu', handleContextMenu);
        };

        document.addEventListener('mouseup', handleMouseUp, { once: true });
        document.addEventListener('contextmenu', handleContextMenu, { once: true });

        // Handle right-click
        if (event.button === 2) {
            setTimeout(function () {
                if (mouseIsDown) {
                    toggleUrlCollection(link, event.target);
                    mouseIsDown = false;
                    isHeld = true;
                }
            }, RIGHT_CLICK_DELAY);
        }
    });
}

function toggleUrlCollection(link, target) {
    if (!link.href) return;

    if (collectedUrls.has(link.href)) {
        // Remove from collection
        const element = collectedUrls.get(link.href);
        try {
            element.classList.remove('hh-collected-link');
        } catch (error) {
            debugError('Failed to remove collected link class:', error);
        }
        collectedUrls.delete(link.href);
        debugLog('Removed URL from collection:', link.href);
    } else {
        // Add to collection
        try {
            target.classList.add('hh-collected-link');
        } catch (error) {
            debugError('Failed to add collected link class:', error);
        }
        collectedUrls.set(link.href, target);
        debugLog('Added URL from collection:', link.href);
    }

    debugLog('Current collection size:', collectedUrls.size);
}

// === SHADOW DOM AND SPECIAL FEATURES ===
function handleShadowRoots() {
    document.addEventListener('mouseover', function (event) {
        if (event.target.shadowRoot && !attachedElements.has(event.target)) {
            attachDragHandler(event.target.shadowRoot);
        }
    });
}

function setupYouTubeFeatures() {
    const domain = document.domain;
    if (domain !== 'www.youtube.com' && domain !== 'm.youtube.com') return;

    const firstChar = (location.host || '').charAt(0);

    if (firstChar === 'w') {
        addYouTubeMenuCommand(
            "Switch to YouTube Mobile (Persistent)",
            "https://m.youtube.com/?persist_app=1&app=m",
            true
        );
        addYouTubeMenuCommand(
            "Switch to YouTube Mobile (Temporary)",
            "https://m.youtube.com/?persist_app=0&app=m",
            false
        );
    } else if (firstChar === 'm') {
        addYouTubeMenuCommand(
            "Switch to YouTube Desktop (Persistent)",
            "https://www.youtube.com/?persist_app=1&app=desktop",
            true
        );
        addYouTubeMenuCommand(
            "Switch to YouTube Desktop (Temporary)",
            "https://www.youtube.com/?persist_app=0&app=desktop",
            false
        );

        // Mobile layout improvements
        try {
            GM_addStyle(`
                ytm-rich-item-renderer {
                    width: 33% !important;
                    margin: 1px !important;
                    padding: 0px !important;
                }
            `);
        } catch (error) {
            debugError('Failed to apply YouTube mobile styles:', error);
        }
    }
}

// === CLEANUP ON PAGE UNLOAD ===
function cleanup() {
    // Clear collections to free memory
    collectedUrls.clear();

    // Remove any visual indicators
    document.querySelectorAll('.hh-collected-link').forEach(el => {
        el.classList.remove('hh-collected-link');
    });

    debugLog('Handlers Helper cleanup completed');
}

// Add cleanup on page unload
window.addEventListener('beforeunload', cleanup);

// === INITIALIZATION ===
function initialize() {
    try {
        setupMenuCommands();
        attachDragHandler(document); // Use standard drag handler
        setupRightClickCollection();
        handleShadowRoots();
        setupYouTubeFeatures();

        debugLog('Handlers Helper (Improved) initialized successfully');
        debugLog('Settings validated and loaded');
    } catch (error) {
        debugError('Initialization failed:', error);
    }
}

// Start the script
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initialize);
} else {
    initialize();
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址