您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A userscript that adds a floating, dockable playlist to YouTube with drag & drop functionality, autoplay, and cross-tab synchronization.
// ==UserScript== // @name YouTube Temporary Playlist (Floating + Dockable + Drag&Drop + Autoplay) // @name:de YouTube Temporary Playlist (Floating + Dockable + Drag&Drop + Autoplay) // @namespace https://greasyfork.org/users/928242 // @version 1.2.0 // @description A userscript that adds a floating, dockable playlist to YouTube with drag & drop functionality, autoplay, and cross-tab synchronization. // @description:de Ein Benutzerskript, das YouTube eine schwebende, andockbare Wiedergabeliste mit Drag & Drop-Funktionalität, Autoplay und tabübergreifender Synchronisierung hinzufügt. // @author Kamikaze (https://github.com/Kamiikaze) // @supportURL https://github.com/Kamiikaze/Tampermonkey/issues // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @match https://www.youtube.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @license MIT // ==/UserScript== (function() { 'use strict'; /** * Fix for TrustedHTML issues in some browsers */ if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) { window.trustedTypes.createPolicy('default', { createHTML: string => string }); } /** * Constants and Configuration */ const CONFIG = { // Storage keys for persistence across tabs/sessions STORAGE: { PLAYLIST: "tempPlaylist", AUTOPLAY: "playlistActive", POSITION: "tempPlaylistPos", DOCKED: "tempPlaylistDocked" }, // UI settings UI: { DEFAULT_WIDTH: "400px", COLLAPSED_WIDTH: "200px", MAX_HEIGHT: "400px", MIN_HEIGHT: "40px", AUTO_HIDE_DELAY: 3000 // 3 seconds delay before auto-hiding } }; /** * State variables */ let state = { playlist: GM_getValue(CONFIG.STORAGE.PLAYLIST, []), autoplayEnabled: GM_getValue(CONFIG.STORAGE.AUTOPLAY, false), savedPos: GM_getValue(CONFIG.STORAGE.POSITION, { left: null, top: null }), isDocked: GM_getValue(CONFIG.STORAGE.DOCKED, false), collapsed: false, isDragging: false, currentDraggedVideoInfo: null, dragOffset: { x: 0, y: 0 }, autoHideTimer: null, videoPlaying: false }; /** * Creates and injects CSS styles for the playlist */ function injectStyles() { const style = document.createElement('style'); style.innerHTML = ` .autoPlaylistOverlay { position: fixed; background: rgba(0,0,0,0.85); color: white; padding: 10px; border-radius: 12px; z-index: 9999; font-size: 14px; display: flex; flex-direction: column; gap: 5px; transition: all 0.3s ease; box-shadow: 0 4px 20px rgba(0,0,0,0.4); } .autoPlaylistOverlay #playlist { display: flex; flex-direction: column; flex: 1; min-height: 100px; border: 1px dashed white; padding: 5px; overflow-y: auto; transition: max-height 0.5s ease; padding-bottom: 50px; scrollbar-width: thin; scrollbar-color: white transparent; } .autoPlaylistOverlay #playlist::-webkit-scrollbar { width: 6px; } .autoPlaylistOverlay #playlist::-webkit-scrollbar-thumb { background: white; border-radius: 10px; } .autoPlaylistOverlay button { padding: 5px 10px; background: rgba(255,255,255,0.1); border: none; border-radius: 8px; color: white; cursor: pointer; transition: background 0.3s; font-weight: bold; } .autoPlaylistOverlay button:hover { background: rgba(255,255,255,0.3); } .autoPlaylistOverlay button[data-index] { padding: 5px; background: none; border: none; border-radius: 8px; color: red; font-size: 16px; cursor: pointer; transition: background 0.3s; } .autoPlaylistOverlay button[data-index]:hover { background: rgba(255, 0, 0, 0.2); } .playlist-item { display: flex; align-items: center; gap: 10px; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); transition: background 0.2s; cursor: pointer; } .playlist-item:hover { background: rgba(255,255,255,0.1); } `; document.head.appendChild(style); } /** * Creates the main floating overlay interface * @returns {Object} References to DOM elements */ function createInterface() { const overlay = document.createElement('div'); overlay.className = "autoPlaylistOverlay"; overlay.innerHTML = ` <div id="header" style="display:flex; justify-content:space-between; align-items:center; cursor:move;"> <h3 id="overlayTitle" style="margin:0; font-size:16px;">AutoPlayList</h3> <button id="collapseButton" style="background:none; border:none; color:white; font-size:18px; cursor:pointer;">_</button> </div> <div id="playlist">Drag Videos Here</div> <div id="controls" style="display:flex; flex-wrap:wrap; justify-content:center; gap:5px;"> <button id="addCurrentVideo" title="Add current video to playlist">Current Video</button> <button id="toggleAutoplay" title="Enables autplay for AutoPlayList after video end">Enable Autoplay</button> <button id="clearPlaylist" title="Delete all items in playlist">Clear Playlist</button> <button id="dockToggle" title="Toggle between docked and moveable overlay">Dock</button> </div> `; document.body.appendChild(overlay); return { overlay, playlistDiv: overlay.querySelector("#playlist"), addCurrentButton: overlay.querySelector("#addCurrentVideo"), toggleButton: overlay.querySelector("#toggleAutoplay"), clearButton: overlay.querySelector("#clearPlaylist"), dockButton: overlay.querySelector("#dockToggle"), collapseButton: overlay.querySelector("#collapseButton"), overlayTitle: overlay.querySelector("#overlayTitle"), controlsDiv: overlay.querySelector("#controls"), header: overlay.querySelector("#header") }; } /** * Storage/Persistence methods */ const storage = { savePlaylist: () => GM_setValue(CONFIG.STORAGE.PLAYLIST, state.playlist), saveAutoplayState: () => GM_setValue(CONFIG.STORAGE.AUTOPLAY, state.autoplayEnabled), savePosition: (left, top) => GM_setValue(CONFIG.STORAGE.POSITION, { left, top }), saveDockState: () => GM_setValue(CONFIG.STORAGE.DOCKED, state.isDocked) }; /** * Updates the playlist container title with current count * @param {Object} elements - DOM elements references */ function updateTitle(elements) { elements.overlayTitle.textContent = `AutoPlayList (${state.playlist.length})`; } /** * Applies docked or floating position based on current state * @param {Object} elements - DOM elements references */ function applyDockState(elements) { const { overlay, dockButton } = elements; const { isDocked, collapsed } = state; if (isDocked) { overlay.style.position = 'fixed'; overlay.style.top = 'unset'; overlay.style.left = 'unset'; overlay.style.bottom = '0'; overlay.style.right = '10px'; overlay.style.borderRadius = '12px 12px 0 0'; overlay.style.width = collapsed ? CONFIG.UI.COLLAPSED_WIDTH : CONFIG.UI.DEFAULT_WIDTH; overlay.style.maxHeight = collapsed ? CONFIG.UI.MIN_HEIGHT : CONFIG.UI.MAX_HEIGHT; overlay.style.height = 'auto'; overlay.style.padding = collapsed ? '5px 10px' : '10px'; dockButton.textContent = 'Undock'; } else { overlay.style.position = 'fixed'; overlay.style.top = 'unset'; overlay.style.left = 'unset'; overlay.style.bottom = '20px'; overlay.style.right = '20px'; overlay.style.borderRadius = '12px'; overlay.style.width = collapsed ? CONFIG.UI.COLLAPSED_WIDTH : CONFIG.UI.DEFAULT_WIDTH; overlay.style.maxHeight = collapsed ? CONFIG.UI.MIN_HEIGHT : CONFIG.UI.MAX_HEIGHT; overlay.style.height = 'auto'; overlay.style.padding = collapsed ? '5px 10px' : '10px'; dockButton.textContent = 'Dock'; } } /** * Ensures the overlay stays within window boundaries * @param {Object} pos - Position coordinates * @param {Object} elements - DOM elements references * @returns {Object} Adjusted position coordinates */ function clampPosition(pos, elements) { const overlayWidth = elements.overlay.offsetWidth; const overlayHeight = elements.overlay.offsetHeight; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; let left = pos.left; let top = pos.top; if (left < 0) left = 0; if (top < 0) top = 0; if (left + overlayWidth > windowWidth) left = windowWidth - overlayWidth-50; if (top + overlayHeight > windowHeight) top = windowHeight - overlayHeight-50; return { left, top }; } /** * Renders the playlist items based on current state * @param {Object} elements - DOM elements references */ function renderPlaylist(elements) { const { playlistDiv } = elements; updateTitle(elements); playlistDiv.innerHTML = ''; if (state.playlist.length === 0) { playlistDiv.textContent = 'Drag Videos Here'; return; } state.playlist.forEach((video, index) => { const thumbnailUrl = `https://i.ytimg.com/vi/${video.id}/hqdefault.jpg`; const item = document.createElement('div'); item.classList.add('playlist-item'); item.draggable = true; item.dataset.index = index; item.innerHTML = ` <div style="font-weight:bold; width:20px; text-align:center;">${index + 1}</div> <img src="${thumbnailUrl}" style="width: 80px; height: 45px; object-fit: cover; border-radius:4px;"> <div style="flex:1; overflow:hidden;"> <div class="title" style=" font-size:14px; font-weight:bold; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; text-overflow: ellipsis; ">${video.title}</div> <div style="font-size:12px; color:lightgray;">${video.channel}</div> </div> <button data-index="${index}" style="color:red;">❌</button> `; setupPlaylistItemEvents(item, index, elements); playlistDiv.appendChild(item); }); } /** * Sets up event handlers for playlist items * @param {HTMLElement} item - The playlist item element * @param {number} index - Index of the item in the playlist * @param {Object} elements - DOM elements references */ function setupPlaylistItemEvents(item, index, elements) { let isItemDragging = false; // Drag & drop reordering item.addEventListener('dragstart', (e) => { isItemDragging = true; e.dataTransfer.setData('text/plain', index); }); item.addEventListener('dragend', () => { isItemDragging = false; }); // Play video on click item.addEventListener('click', (e) => { if (isItemDragging || e.target.tagName === 'BUTTON') return; window.location.href = state.playlist[index].url; }); // Title expansion on hover item.addEventListener('mouseenter', () => { const title = item.querySelector('.title'); title.style.overflow = 'visible'; title.style.webkitLineClamp = 'unset'; }); item.addEventListener('mouseleave', () => { const title = item.querySelector('.title'); title.style.overflow = 'hidden'; title.style.webkitLineClamp = '2'; }); // Item reordering via drag & drop item.addEventListener('dragover', (e) => { e.preventDefault(); item.style.background = 'rgba(255,255,255,0.1)'; }); item.addEventListener('dragleave', () => { item.style.background = 'rgba(255,255,255,0.05)'; }); item.addEventListener('drop', (e) => { e.preventDefault(); item.style.background = 'rgba(255,255,255,0.05)'; const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10); const toIndex = parseInt(item.dataset.index, 10); if (fromIndex !== toIndex) { const movedItem = state.playlist.splice(fromIndex, 1)[0]; state.playlist.splice(toIndex, 0, movedItem); storage.savePlaylist(); renderPlaylist(elements); } }); } /** * Sets up the overlay drag functionality with improved performance * @param {Object} elements - DOM elements references */ function setupOverlayDrag(elements) { const { header, overlay } = elements; let rafId = null; // Use requestAnimationFrame for smooth rendering function updatePosition(x, y) { if (rafId) { cancelAnimationFrame(rafId); } rafId = requestAnimationFrame(() => { const left = x - state.dragOffset.x; const top = y - state.dragOffset.y; overlay.style.left = `${left}px`; overlay.style.top = `${top}px`; overlay.style.bottom = 'unset'; overlay.style.right = 'unset'; rafId = null; }); } header.addEventListener('mousedown', (e) => { if (state.isDocked || e.target.id === 'collapseButton') return; state.isDragging = true; // Calculate offset relative to the header const rect = overlay.getBoundingClientRect(); state.dragOffset.x = e.clientX - rect.left; state.dragOffset.y = e.clientY - rect.top; // Make sure we're using fixed positioning overlay.style.position = 'fixed'; overlay.style.bottom = 'unset'; overlay.style.right = 'unset'; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', (e) => { if (state.isDocked || !state.isDragging) return; updatePosition(e.clientX, e.clientY); }); document.addEventListener('mouseup', () => { if (!state.isDragging) return; state.isDragging = false; document.body.style.userSelect = ''; if (!state.isDocked) { // Apply clamping to ensure we stay within window boundaries const left = parseFloat(overlay.style.left) || 0; const top = parseFloat(overlay.style.top) || 0; const finalPosition = clampPosition({ left: left, top: top }, elements); // Apply the clamped position overlay.style.left = `${finalPosition.left}px`; overlay.style.top = `${finalPosition.top}px`; storage.savePosition(finalPosition.left, finalPosition.top); } }); } /** * Sets up behavior for docking the overlay * @param {Object} elements - DOM elements references */ function setupDockButton(elements) { const { dockButton } = elements; dockButton.addEventListener('click', () => { state.isDocked = !state.isDocked; storage.saveDockState(); applyDockState(elements); }); } /** * Sets up collapse/expand behavior * @param {Object} elements - DOM elements references */ function setupCollapseButton(elements) { const { collapseButton, playlistDiv, controlsDiv, overlay } = elements; collapseButton.addEventListener('click', (e) => { e.stopPropagation(); toggleCollapse(elements); }); } /** * Toggles the collapsed state of the playlist * @param {Object} elements - DOM elements references * @param {boolean} [forceCollapse] - Force a specific collapse state */ function toggleCollapse(elements, forceCollapse = null) { const { playlistDiv, controlsDiv, overlay } = elements; if (forceCollapse !== null) { state.collapsed = forceCollapse; } else { state.collapsed = !state.collapsed; } if (state.collapsed) { playlistDiv.style.maxHeight = '0'; playlistDiv.style.display = 'none'; controlsDiv.style.display = 'none'; overlay.style.width = CONFIG.UI.COLLAPSED_WIDTH; overlay.style.padding = '5px 10px'; if (state.isDocked) { overlay.style.bottom = '0'; overlay.style.right = '10px'; overlay.style.borderRadius = '12px 12px 0 0'; } } else { playlistDiv.style.maxHeight = '300px'; playlistDiv.style.display = 'flex'; controlsDiv.style.display = 'flex'; overlay.style.width = CONFIG.UI.DEFAULT_WIDTH; overlay.style.padding = '10px'; if (state.isDocked) { overlay.style.bottom = '0'; overlay.style.right = '10px'; overlay.style.borderRadius = '12px 12px 0 0'; } } } /** * Sets up drag & drop to add videos to playlist * @param {Object} elements - DOM elements references */ function setupPlaylistDrop(elements) { const { playlistDiv } = elements; // Handle dragover events playlistDiv.addEventListener('dragover', (e) => { e.preventDefault(); playlistDiv.style.background = 'rgba(0, 150, 255, 0.2)'; playlistDiv.style.border = '2px dashed #00f'; }); // Reset styles when drag leaves playlistDiv.addEventListener('dragleave', () => { playlistDiv.style.background = ''; playlistDiv.style.border = '1px dashed white'; }); // Process dropped items playlistDiv.addEventListener('drop', (e) => { e.preventDefault(); playlistDiv.style.background = ''; playlistDiv.style.border = '1px dashed white'; if (state.currentDraggedVideoInfo) { state.playlist.push(state.currentDraggedVideoInfo); storage.savePlaylist(); renderPlaylist(elements); state.currentDraggedVideoInfo = null; } else { const data = e.dataTransfer.getData('text/plain'); if (data && data.includes('youtube.com/watch')) { const videoId = (new URL(data)).searchParams.get('v') || ''; state.playlist.push({ url: data, id: videoId, title: "Unknown Title", channel: "Unknown Channel" }); storage.savePlaylist(); renderPlaylist(elements); } } }); } /** * Sets up delete functionality for playlist items * @param {Object} elements - DOM elements references */ function setupPlaylistDelete(elements) { const { playlistDiv } = elements; playlistDiv.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON' && e.target.hasAttribute('data-index')) { const idx = parseInt(e.target.getAttribute('data-index'), 10); if (!isNaN(idx)) { state.playlist.splice(idx, 1); storage.savePlaylist(); renderPlaylist(elements); } e.stopPropagation(); } }); } /** * Extracts current video information from the YouTube page * @returns {Object|null} Video information or null if not on a video page */ function getCurrentVideoInfo() { // Check if we're on a video page if (!window.location.pathname.includes('/watch')) { return null; } try { // Get video ID from URL const videoId = new URL(window.location.href).searchParams.get('v'); if (!videoId) return null; // Get video title const titleElement = document.querySelector('h1.ytd-watch-metadata'); const title = titleElement ? titleElement.textContent.trim() : 'Unknown Title'; // Get channel name const channelElement = document.querySelector('ytd-channel-name#channel-name a, #owner-name a'); const channel = channelElement ? channelElement.textContent.trim() : 'Unknown Channel'; return { url: window.location.href, id: videoId, title: title, channel: channel }; } catch (error) { console.error("Error getting current video info:", error); return null; } } /** * Adds current video button to the controls * @param {Object} elements - DOM elements references */ function setupCurrentVideoButton(elements) { const { addCurrentButton } = elements; // Add icon for better visibility addCurrentButton.innerHTML = `<span style="font-size:14px;">➕</span> Current Video`; // Setup click handler addCurrentButton.addEventListener('click', () => { const videoInfo = getCurrentVideoInfo(); if (videoInfo) { // Check if video is already in playlist const existingIndex = state.playlist.findIndex(v => v.id === videoInfo.id); if (existingIndex >= 0) { // Video already exists - provide visual feedback addCurrentButton.textContent = 'Already Added'; addCurrentButton.style.background = 'rgba(255,255,255,0.2)'; // Reset button after a short delay setTimeout(() => { addCurrentButton.innerHTML = `<span style="font-size:14px;">➕</span> Current Video`; addCurrentButton.style.background = 'rgba(255,255,255,0.1)'; }, 1500); return; } // Add to playlist state.playlist.push(videoInfo); storage.savePlaylist(); renderPlaylist(elements); // Provide visual feedback addCurrentButton.textContent = 'Added! ✓'; addCurrentButton.style.background = 'rgba(0,255,0,0.2)'; // Reset button after a short delay setTimeout(() => { addCurrentButton.innerHTML = `<span style="font-size:14px;">➕</span> Current Video`; addCurrentButton.style.background = 'rgba(255,255,255,0.1)'; }, 1500); } else { // Not on a video page or couldn't get info addCurrentButton.textContent = 'No Video Found'; addCurrentButton.style.background = 'rgba(255,0,0,0.2)'; // Reset button after a short delay setTimeout(() => { addCurrentButton.innerHTML = `<span style="font-size:14px;">➕</span> Current Video`; addCurrentButton.style.background = 'rgba(255,255,255,0.1)'; }, 1500); } }); // Update button visibility based on page type function updateButtonVisibility() { const isVideoPage = window.location.pathname.includes('/watch'); addCurrentButton.style.opacity = isVideoPage ? '1' : '0.5'; } // Initial visibility update updateButtonVisibility(); // Listen for navigation changes const observer = new MutationObserver(() => { updateButtonVisibility(); }); observer.observe(document.querySelector('head > title'), { subtree: true, characterData: true, childList: true }); } /** * Sets up autoplay toggle functionality * @param {Object} elements - DOM elements references */ function setupAutoplayToggle(elements) { const { toggleButton } = elements; toggleButton.addEventListener('click', () => { state.autoplayEnabled = !state.autoplayEnabled; toggleButton.textContent = state.autoplayEnabled ? "Disable Autoplay" : "Enable Autoplay"; storage.saveAutoplayState(); }); } /** * Sets up playlist clear button * @param {Object} elements - DOM elements references */ function setupClearButton(elements) { const { clearButton } = elements; clearButton.addEventListener('click', () => { state.playlist = []; storage.savePlaylist(); renderPlaylist(elements); }); } /** * Sets up autoplay functionality for videos */ function setupVideoAutoplay() { const observer = new MutationObserver(() => { const video = document.querySelector('video'); if (!video) return; video.addEventListener('ended', () => { if (!state.autoplayEnabled || state.playlist.length === 0) return; const currentUrl = location.href; const nextIndex = state.playlist.findIndex(v => currentUrl.includes(v.id)); if (nextIndex >= 0 && nextIndex < state.playlist.length - 1) { // Add a small delay before navigating to next video setTimeout(() => { window.location.href = state.playlist[nextIndex + 1].url; }, 300); } else { state.autoplayEnabled = false; storage.saveAutoplayState(); document.querySelector("#toggleAutoplay").textContent = "Enable Autoplay"; } }, { once: true }); }); observer.observe(document.body, { childList: true, subtree: true }); } /** * Setup auto-hide functionality when video is playing * @param {Object} elements - DOM elements references */ function setupAutoHideOnPlay(elements) { const { overlay } = elements; // Video play/pause state detection const observeVideo = () => { const observer = new MutationObserver(() => { const video = document.querySelector('video'); if (!video) return; // Setup play/pause event listeners if not already attached if (!video.dataset.playlistEventsBound) { video.dataset.playlistEventsBound = "true"; // When video plays, dock and collapse after delay video.addEventListener('play', () => { state.videoPlaying = true; // Clear any existing timer if (state.autoHideTimer) { clearTimeout(state.autoHideTimer); } // Set timer to hide after delay state.autoHideTimer = setTimeout(() => { if (!state.isDocked) { state.isDocked = true; storage.saveDockState(); applyDockState(elements); } if (!state.collapsed) { toggleCollapse(elements, true); // Force collapse } }, CONFIG.UI.AUTO_HIDE_DELAY); }); // When video pauses, show the playlist again video.addEventListener('pause', () => { state.videoPlaying = false; // Clear any hide timer if (state.autoHideTimer) { clearTimeout(state.autoHideTimer); state.autoHideTimer = null; } // Only expand if it was auto-collapsed (while playing) if (state.collapsed && !state.isDragging) { toggleCollapse(elements, false); // Force expand } }); } }); observer.observe(document.body, { childList: true, subtree: true }); }; // Start observing for video elements observeVideo(); // Also handle navigation events to reattach listeners window.addEventListener('yt-navigate-finish', () => { // Reset bound state so we can reattach listeners const video = document.querySelector('video'); if (video) { delete video.dataset.playlistEventsBound; } observeVideo(); }); } /** * Sets up handling for YouTube video dragging */ function setupYouTubeDragHandling() { document.addEventListener('dragstart', (e) => { const videoEl = e.target.closest('ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer, ytd-playlist-video-renderer'); if (videoEl) { const anchor = videoEl.querySelector('a[href*="/watch?v="]'); if (!anchor) return; const videoUrl = anchor.href; const videoId = (new URL(videoUrl)).searchParams.get('v') || ''; const titleEl = videoEl.querySelector('#video-title, yt-formatted-string#video-title'); const title = titleEl ? titleEl.textContent.trim() : 'Unknown Title'; const channelEl = videoEl.querySelector('ytd-channel-name a, .ytd-channel-name #text'); const channel = channelEl ? channelEl.textContent.trim() : 'Unknown Channel'; state.currentDraggedVideoInfo = { url: videoUrl, id: videoId, title: title, channel: channel }; } }); } /** * Sets up synchronization across tabs * @param {Object} elements - DOM elements references */ function setupCrossBrowserSync(elements) { GM_addValueChangeListener(CONFIG.STORAGE.PLAYLIST, (_, __, newValue) => { state.playlist = newValue; renderPlaylist(elements); }); GM_addValueChangeListener(CONFIG.STORAGE.AUTOPLAY, (_, __, newValue) => { state.autoplayEnabled = newValue; elements.toggleButton.textContent = state.autoplayEnabled ? "Disable Autoplay" : "Enable Autoplay"; }); GM_addValueChangeListener(CONFIG.STORAGE.POSITION, (_, __, newValue) => { if (state.isDocked) return; const clamped = clampPosition(newValue, elements); elements.overlay.style.left = `${clamped.left}px`; elements.overlay.style.top = `${clamped.top}px`; }); GM_addValueChangeListener(CONFIG.STORAGE.DOCKED, (_, __, newValue) => { state.isDocked = newValue; applyDockState(elements); }); } /** * Handles window resize events * @param {Object} elements - DOM elements references */ function setupWindowEvents(elements) { const { overlay } = elements; // Handle window resize window.addEventListener('resize', () => { if (state.isDocked) return; const clamped = clampPosition({ left: overlay.offsetLeft, top: overlay.offsetTop }, elements); overlay.style.left = `${clamped.left}px`; overlay.style.top = `${clamped.top}px`; storage.savePosition(clamped.left, clamped.top); }); // Hide overlay during fullscreen video document.addEventListener('fullscreenchange', () => { const isFullscreen = !!document.fullscreenElement; overlay.style.display = isFullscreen ? 'none' : 'flex'; }); } /** * Restores the overlay position from saved state * @param {Object} elements - DOM elements references */ function restorePosition(elements) { const { overlay } = elements; const { savedPos } = state; if (!state.isDocked && savedPos.left !== null && savedPos.top !== null) { const clamped = clampPosition(savedPos, elements); overlay.style.left = `${clamped.left}px`; overlay.style.top = `${clamped.top}px`; overlay.style.bottom = 'unset'; overlay.style.right = 'unset'; } } /** * Main initialization function */ function init() { // Setup UI injectStyles(); const elements = createInterface(); // Apply initial state applyDockState(elements); renderPlaylist(elements); restorePosition(elements); // Set initial autoplay button text elements.toggleButton.textContent = state.autoplayEnabled ? "Disable Autoplay" : "Enable Autoplay"; // Setup event handlers setupOverlayDrag(elements); setupCollapseButton(elements); setupPlaylistDrop(elements); setupPlaylistDelete(elements); setupCurrentVideoButton(elements); setupAutoplayToggle(elements); setupClearButton(elements); setupDockButton(elements); setupVideoAutoplay(); setupAutoHideOnPlay(elements); setupYouTubeDragHandling(); setupCrossBrowserSync(elements); setupWindowEvents(elements); } // Start everything init(); })();