您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); }