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.

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name        Handlers Helper (Improved)
// @namespace   Violentmonkey Scripts
// @version     4.9.1
// @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';

/**
 * Handlers Helper (Improved) - Modular Version (Fixed Dependencies)
 *
 * This userscript provides enhanced drag-to-action functionality for media links
 * with MPV integration and multiple protocol support.
 *
 * Architecture:
 * - Constants: Configuration and default values (no deps)
 * - Utils: Utility functions (no deps)
 * - State: Global state management (depends on Utils)
 * - LiveChat: Live chat integration (depends on Utils, Constants)
 * - Actions: Action execution logic (depends on Utils, State, LiveChat)
 * - Drag: Drag handling and direction calculation (depends on Utils, State, Constants, Actions)
 * - Menu: Menu command setup (depends on Utils, State, Constants, Drag)
 * - Collection: URL collection system (depends on Utils, State)
 * - YouTube: YouTube-specific features (depends on Utils)
 * - Main: Initialization and orchestration (depends on all)
 */

// ===== MODULE: CONSTANTS =====
const Constants = (() => {
    'use strict';

    const GUIDE = 'Value: pipe ytdl stream mpv iptv';

    const ACTION_EXPLANATIONS = {
        pipe: '📺 pipe (UP) → mpv://mpvy/ → Pipe video to MPV with yt-dlp processing',
        ytdl: '📥 ytdl (DOWN) → mpv://ytdl/ → Download video using yt-dlp',
        stream: '🌊 stream (LEFT) → mpv://stream/ → Stream video using streamlink',
        mpv: '▶️ mpv (RIGHT) → mpv://play/ → Direct play in MPV player',
        iptv: '📋 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;

    const DEFAULTS = {
        UP: 'pipe',
        DOWN: 'ytdl',
        LEFT: 'stream',
        RIGHT: 'mpv',
        hlsdomain: 'cdn.animevui.com',
        livechat: false,
        total_direction: 4,
        down_confirm: true,
        debug: false
    };

    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
    });

    return {
        GUIDE,
        ACTION_EXPLANATIONS,
        LIVE_WINDOW_WIDTH,
        LIVE_WINDOW_HEIGHT,
        DRAG_THRESHOLD,
        RIGHT_CLICK_DELAY,
        DEFAULTS,
        DirectionEnum
    };
})();

// ===== MODULE: UTILS =====
const Utils = (() => {
    'use strict';

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

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

    const debugLog = (...args) => {
        // Check if State is available and debug is enabled
        if (typeof State !== 'undefined' && State.settings && State.settings.debug) {
            console.log(...args);
        }
    };

    const debugWarn = (...args) => {
        if (typeof State !== 'undefined' && State.settings && State.settings.debug) {
            console.warn(...args);
        }
    };

    const debugError = (...args) => {
        if (typeof State !== 'undefined' && State.settings && State.settings.debug) {
            console.error(...args);
        } else {
            // Always log errors even if debug is off
            console.error(...args);
        }
    };

    const 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;
    };

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

    return {
        safePrompt,
        reloadPage,
        debugLog,
        debugWarn,
        debugError,
        getParentByTagName,
        encodeUrl
    };
})();

// ===== MODULE: STATE =====
const State = (() => {
    'use strict';

    let settings = {};
    let hlsdomainArray = [];
    let collectedUrls = new Map();
    let attachedElements = new WeakSet();

    const init = () => {
        settings = {
            UP: GM_getValue('UP', Constants.DEFAULTS.UP),
            DOWN: GM_getValue('DOWN', Constants.DEFAULTS.DOWN),
            LEFT: GM_getValue('LEFT', Constants.DEFAULTS.LEFT),
            RIGHT: GM_getValue('RIGHT', Constants.DEFAULTS.RIGHT),
            hlsdomain: GM_getValue('hlsdomain', Constants.DEFAULTS.hlsdomain),
            livechat: GM_getValue('livechat', Constants.DEFAULTS.livechat),
            total_direction: GM_getValue('total_direction', Constants.DEFAULTS.total_direction),
            down_confirm: GM_getValue('down_confirm', Constants.DEFAULTS.down_confirm),
            debug: GM_getValue('debug', Constants.DEFAULTS.debug)
        };

        hlsdomainArray = settings.hlsdomain.split(',').filter(d => d.trim());

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

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

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

    return {
        init,
        get settings() { return settings; },
        get hlsdomainArray() { return hlsdomainArray; },
        set hlsdomainArray(value) { hlsdomainArray = value; },
        get collectedUrls() { return collectedUrls; },
        get attachedElements() { return attachedElements; },
        updateSetting
    };
})();

// ===== MODULE: LIVECHAT =====
const LiveChat = (() => {
    'use strict';

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

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

    const 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) {
                    Utils.debugError('Nimo.tv chat extraction failed:', error);
                }
            }
        } catch (error) {
            Utils.debugError('Live chat opener failed:', error);
        }
    };

    return {
        openLiveChat
    };
})();

// ===== MODULE: ACTIONS =====
const Actions = (() => {
    'use strict';

    const executeAction = (targetUrl, actionType) => {
        Utils.debugLog('Executing action:', actionType, 'for URL:', targetUrl);

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

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

        // Check HLS domains
        for (const domain of State.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) {
                Utils.debugError('Failed to navigate to mpv URL:', error);
            }
            return;
        } else {
            finalUrl = location.href;
        }

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

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

            State.collectedUrls.clear();
            Utils.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 = Utils.encodeUrl(urlString);
        const encodedReferer = Utils.encodeUrl(location.href);
        let protocolUrl = `mpv://${app}/${encodedUrl}/?referer=${encodedReferer}`;

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

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

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

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

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

    return {
        executeAction
    };
})();

// ===== MODULE: DRAG =====
const Drag = (() => {
    'use strict';

    const getDirection = (startX, startY, endX, endY) => {
        const deltaX = endX - startX;
        const deltaY = endY - startY;

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

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

        let direction;

        if (State.settings.total_direction === 4) {
            // 4-direction mode
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                direction = deltaX > 0 ? Constants.DirectionEnum.RIGHT : Constants.DirectionEnum.LEFT;
            } else {
                direction = deltaY > 0 ? Constants.DirectionEnum.DOWN : Constants.DirectionEnum.UP;
            }
            Utils.debugLog('4-direction mode, result:', direction, direction === Constants.DirectionEnum.UP ? '(UP)' : direction === Constants.DirectionEnum.DOWN ? '(DOWN)' : direction === Constants.DirectionEnum.LEFT ? '(LEFT)' : '(RIGHT)');
        } else {
            // 8-direction mode
            if (deltaX === 0) {
                direction = deltaY > 0 ? Constants.DirectionEnum.DOWN : Constants.DirectionEnum.UP;
                Utils.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 ? Constants.DirectionEnum.RIGHT : Constants.DirectionEnum.LEFT;
                } else if (absSlope > 2.4142) { // ~67.5 degrees
                    direction = deltaY > 0 ? Constants.DirectionEnum.DOWN : Constants.DirectionEnum.UP;
                } else {
                    // Diagonal directions
                    if (deltaX > 0) {
                        direction = deltaY > 0 ? Constants.DirectionEnum.DOWN_RIGHT : Constants.DirectionEnum.UP_RIGHT;
                    } else {
                        direction = deltaY > 0 ? Constants.DirectionEnum.DOWN_LEFT : Constants.DirectionEnum.UP_LEFT;
                    }
                }
                Utils.debugLog('8-direction mode, slope:', slope, 'result:', direction);
            }
        }

        return direction;
    };

    const 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;
    };

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

    const 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();
        }
    };

    const attachDragHandler = (element) => {
        if (!element || State.attachedElements.has(element)) return;

        State.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;

                Utils.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;

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

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

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

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

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

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

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

    return {
        getDirection,
        attachDragHandler
    };
})();

// ===== MODULE: MENU =====
const Menu = (() => {
    'use strict';

    const showActionHelp = () => {
        const helpText = `🎮 DRAG DIRECTIONS & ACTIONS:

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

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

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

▶️ RIGHT (→): ${State.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: ${State.settings.total_direction} directions, HLS: ${State.settings.hlsdomain.split(',').length} domains

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

        alert(helpText);
    };

    const testDirections = () => {
        Utils.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 = Drag.getDirection(test.start[0], test.start[1], test.end[0], test.end[1]);
            Utils.debugLog(`${test.name}: (${test.start[0]},${test.start[1]}) -> (${test.end[0]},${test.end[1]}) = Direction ${direction}`);
        });
    };

    const setupMenuCommands = () => {
        // Help command first
        GM_registerMenuCommand('❓ Show Action Help', showActionHelp);
        GM_registerMenuCommand('🧪 Test Directions', testDirections);

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

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

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

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

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

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

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

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

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

    return {
        setupMenuCommands
    };
})();

// ===== MODULE: COLLECTION =====
const Collection = (() => {
    'use strict';

    const toggleUrlCollection = (link, target) => {
        if (!link.href) return;

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

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

    const setupRightClickCollection = () => {
        let mouseIsDown = false;
        let isHeld = false;

        document.addEventListener('mousedown', function (event) {
            const link = Utils.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;
                    }
                }, Constants.RIGHT_CLICK_DELAY);
            }
        });
    };

    return {
        setupRightClickCollection
    };
})();

// ===== MODULE: YOUTUBE =====
const YouTube = (() => {
    'use strict';

    const 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) {
                Utils.debugError('Failed to navigate:', error);
            }
        });
    };

    const 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) {
                Utils.debugError('Failed to apply YouTube mobile styles:', error);
            }
        }
    };

    return {
        setupYouTubeFeatures
    };
})();

// ===== MODULE: MAIN =====
const Main = (() => {
    'use strict';

    const handleShadowRoots = () => {
        document.addEventListener('mouseover', function (event) {
            if (event.target.shadowRoot && !State.attachedElements.has(event.target)) {
                Drag.attachDragHandler(event.target.shadowRoot);
            }
        });
    };

    const cleanup = () => {
        // Clear collections to free memory
        State.collectedUrls.clear();

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

        Utils.debugLog('Handlers Helper cleanup completed');
    };

    const initialize = () => {
        try {
            State.init();
            Menu.setupMenuCommands();
            Drag.attachDragHandler(document);
            Collection.setupRightClickCollection();
            handleShadowRoots();
            YouTube.setupYouTubeFeatures();

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

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

    return {
        initialize
    };
})();

// ===== INITIALIZATION =====
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', Main.initialize);
} else {
    Main.initialize();
}