您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Navigate YouTube with leader key 'i' followed by other keys
// ==UserScript== // @name YouTube Hotkeys // @namespace Violentmonkey Scripts // @version 2.0 // @description Navigate YouTube with leader key 'i' followed by other keys // @author dpi0 // @author You // @match https://www.youtube.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant window.close // @homepageURL https://github.com/dpi0/scripts/blob/main/greasyfork/youtube-hotkeys.js // @supportURL https://github.com/dpi0/scripts/issues // @license MIT // ==/UserScript== (function () { "use strict"; // Configuration with default values const DEFAULT_LEADER_KEY = "i"; const TIMEOUT = 2000; // Time window in ms to press the second key after leader key // Get leader key from GM storage, or use default if not set let LEADER_KEY = GM_getValue("leaderKey", DEFAULT_LEADER_KEY); // Setup Violentmonkey/Tampermonkey menu commands GM_registerMenuCommand("🔑 Change Leader Key", promptForLeaderKey); GM_registerMenuCommand("🗘 Reset Leader Key", resetLeaderKey); // Function to prompt user for new leader key function promptForLeaderKey() { const newKey = prompt("Enter a new leader key:", LEADER_KEY); if (newKey && newKey.length === 1) { LEADER_KEY = newKey.toLowerCase(); GM_setValue("leaderKey", LEADER_KEY); showNotification(`Leader key changed to '${LEADER_KEY}'`); } else if (newKey) { alert("Leader key must be a single character."); } } // Function to reset leader key to default function resetLeaderKey() { LEADER_KEY = DEFAULT_LEADER_KEY; GM_setValue("leaderKey", LEADER_KEY); showNotification(`Leader key reset to '${LEADER_KEY}'`); } // Navigation and action functions function navigateToHome() { window.location.href = "https://www.youtube.com/"; } function navigateToSubscriptions() { window.location.href = "https://www.youtube.com/feed/subscriptions"; } function navigateToHistory() { window.location.href = "https://www.youtube.com/feed/history"; } function navigateToWatchLater() { window.location.href = "https://www.youtube.com/playlist?list=WL"; } function navigateToLikedVideos() { window.location.href = "https://www.youtube.com/playlist?list=LL"; } function navigateToTrending() { window.location.href = "https://www.youtube.com/feed/trending"; } function navigateToLibrary() { window.location.href = "https://www.youtube.com/feed/library"; } function navigateToChannelVideos() { // Fixed function to work on both channel pages and video pages if (window.location.pathname.includes("/watch")) { // If on a video page, find the channel link const channelLink = document.querySelector("#top-row ytd-video-owner-renderer a") || document.querySelector("ytd-channel-name a") || document.querySelector("a.ytd-channel-name"); if (channelLink) { // Get the channel URL and append /videos let channelUrl = channelLink.href; if (!channelUrl.endsWith("/videos")) { channelUrl = channelUrl.split("?")[0]; // Remove any query parameters channelUrl = channelUrl.endsWith("/") ? channelUrl + "videos" : channelUrl + "/videos"; } window.location.href = channelUrl; } else { showNotification("Channel link not found on this video page!"); } } else if ( window.location.pathname.includes("/channel/") || window.location.pathname.includes("/c/") || window.location.pathname.includes("/user/") || window.location.pathname.includes("/@") ) { // If already on a channel page, navigate to videos section // Extract the channel name/ID from the URL const channelPath = window.location.pathname.split("/")[1]; // Get the @CHANNEL_NAME part window.location.href = `https://www.youtube.com/${channelPath}/videos`; } else { showNotification("Not on a video or channel page!"); } } function navigateToChannelPlaylists() { // Fixed function to work on both channel pages and video pages if (window.location.pathname.includes("/watch")) { // If on a video page, find the channel link const channelLink = document.querySelector("#top-row ytd-video-owner-renderer a") || document.querySelector("ytd-channel-name a") || document.querySelector("a.ytd-channel-name"); if (channelLink) { // Get the channel URL and append /playlists let channelUrl = channelLink.href; if (!channelUrl.endsWith("/playlists")) { channelUrl = channelUrl.split("?")[0]; // Remove any query parameters channelUrl = channelUrl.endsWith("/") ? channelUrl + "playlists" : channelUrl + "/playlists"; } window.location.href = channelUrl; } else { showNotification("Channel link not found on this video page!"); } } else if ( window.location.pathname.includes("/channel/") || window.location.pathname.includes("/c/") || window.location.pathname.includes("/user/") || window.location.pathname.includes("/@") ) { // If already on a channel page, navigate to playlists section // Extract the channel name/ID from the URL const channelPath = window.location.pathname.split("/")[1]; // Get the @CHANNEL_NAME part window.location.href = `https://www.youtube.com/${channelPath}/playlists`; } else { showNotification("Not on a video or channel page!"); } } function triggerSaveButton() { // Only works on watch pages if (!window.location.pathname.includes("/watch")) { showNotification("This only works on video pages!"); return; } // Try to find the Save button using various selectors const saveButton = document.querySelector('button[aria-label="Save to playlist"]') || document.querySelector('ytd-button-renderer[id="save-button"]') || document.querySelector('ytd-menu-renderer button[aria-label="Save"]') || document.querySelector('button.ytd-menu-renderer[aria-label="Save"]') || document.querySelector('button[aria-label="Save"]'); if (saveButton) { saveButton.click(); showNotification("Save to playlist popup triggered"); } else { showNotification("Save button not found!"); } } function navigateToNextVideo() { // Only works on watch pages if (!window.location.pathname.includes("/watch")) { showNotification("This only works on video pages!"); return; } // Try to find the "Next" button and click it const nextButton = findNextButton(); if (nextButton) { nextButton.click(); // No need for notification as the page will navigate } else { showNotification("Next video button not found!"); } } function navigateToPreviousVideo() { // Only works on watch pages if (!window.location.pathname.includes("/watch")) { showNotification("This only works on video pages!"); return; } // YouTube doesn't have a standard "Previous video" button // This is just a placeholder, as YouTube doesn't have a native "previous video" button showNotification("Previous video navigation not supported by YouTube"); } function toggleSidebar() { // Find and click the guide button (hamburger menu) const guideButton = document.querySelector("#guide-button") || document.querySelector('button[aria-label="Guide"]') || document.querySelector('button[aria-label="Menu"]'); if (guideButton) { guideButton.click(); showNotification("Toggled sidebar"); } else { showNotification("Sidebar toggle button not found!"); } } function copyVideoUrlWithTimestamp() { // Only works on watch pages if (!window.location.pathname.includes("/watch")) { showNotification("This only works on video pages!"); return; } // Get current video time const video = document.querySelector("video"); if (!video) { showNotification("Video element not found!"); return; } const currentTime = Math.floor(video.currentTime); const currentUrl = window.location.href.split("&t=")[0]; // Remove any existing timestamp const urlWithTimestamp = `${currentUrl}&t=${currentTime}s`; // Copy to clipboard try { navigator.clipboard .writeText(urlWithTimestamp) .then(() => { showNotification("Video URL with timestamp copied to clipboard!"); }) .catch((err) => { console.error("Failed to copy: ", err); showNotification("Failed to copy URL"); }); } catch (e) { // Fallback for browsers that don't support clipboard API const textarea = document.createElement("textarea"); textarea.value = urlWithTimestamp; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); showNotification("Video URL with timestamp copied to clipboard!"); } } // Helper function to find the Next button function findNextButton() { // YouTube's UI changes frequently, so we need multiple selectors const selectors = [ ".ytp-next-button", // Old UI next button "a.ytp-next-button", // Another variation ".ytd-watch-next-secondary-results-renderer button", // Newer UI 'button[aria-label="Next"]', // Generic aria-label approach 'ytd-button-renderer button[aria-label="Next"]', // More specific // Add more selectors as YouTube's UI changes ]; for (const selector of selectors) { const button = document.querySelector(selector); if (button) return button; } return null; } function copyShortenedUrl() { if (!window.location.pathname.includes("/watch")) { showNotification("This only works on video pages!"); return; } const urlParams = new URLSearchParams(window.location.search); const videoId = urlParams.get("v"); if (!videoId) { showNotification("Video ID not found!"); return; } const shortUrl = `https://youtu.be/${videoId}`; try { navigator.clipboard .writeText(shortUrl) .then(() => { showNotification("Shortened URL copied to clipboard!"); }) .catch((err) => { console.error("Clipboard write failed:", err); fallbackCopyToClipboard(shortUrl); }); } catch (e) { fallbackCopyToClipboard(shortUrl); } function fallbackCopyToClipboard(text) { const textarea = document.createElement("textarea"); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); showNotification("Shortened URL copied to clipboard!"); } } function navigateToCommunityTab() { const base = window.location.origin; let channelPath = null; if (window.location.pathname.includes("/watch")) { const channelLink = document.querySelector("#top-row ytd-video-owner-renderer a") || document.querySelector("ytd-channel-name a") || document.querySelector("a.ytd-channel-name"); if (channelLink) { const url = new URL(channelLink.href); channelPath = url.pathname; } } else { const match = window.location.pathname.match( /^\/(channel|c|user|@[^\/]+)(\/.*)?$/, ); if (match) { channelPath = `/${match[1]}`; } } if (channelPath) { window.location.href = `${base}${channelPath}/community`; } else { showNotification("Unable to resolve channel path for community tab."); } } function showHelpModal() { // Remove existing modal if present const existing = document.getElementById("yt-hotkey-help-modal"); if (existing) existing.remove(); // Create overlay const overlay = document.createElement("div"); overlay.id = "yt-hotkey-help-modal"; overlay.style.position = "fixed"; overlay.style.top = "0"; overlay.style.left = "0"; overlay.style.width = "100vw"; overlay.style.height = "100vh"; overlay.style.backgroundColor = "rgba(0, 0, 0, 0.6)"; overlay.style.zIndex = "10000"; overlay.style.display = "flex"; overlay.style.justifyContent = "center"; overlay.style.alignItems = "center"; // Modal content const modal = document.createElement("div"); modal.style.backgroundColor = "#fff"; modal.style.borderRadius = "8px"; modal.style.padding = "20px 30px"; modal.style.maxWidth = "600px"; modal.style.maxHeight = "80vh"; modal.style.overflowY = "auto"; modal.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)"; modal.style.fontFamily = "Arial, sans-serif"; const title = document.createElement("h2"); title.textContent = "YouTube Leader Key Hotkeys"; title.style.marginTop = "0"; const table = document.createElement("table"); table.style.width = "100%"; table.style.borderCollapse = "collapse"; const rows = Object.entries(HOTKEYS).map(([key, fn]) => { const row = document.createElement("tr"); const keyCell = document.createElement("td"); keyCell.textContent = `i + ${key}`; keyCell.style.fontWeight = "bold"; keyCell.style.padding = "4px 8px"; keyCell.style.borderBottom = "1px solid #ddd"; keyCell.style.whiteSpace = "nowrap"; const descCell = document.createElement("td"); descCell.textContent = fn.name .replace(/navigateTo|copy|toggle|trigger|show/i, "") .replace(/([A-Z])/g, " $1") .trim(); descCell.style.padding = "4px 8px"; descCell.style.borderBottom = "1px solid #ddd"; descCell.style.textTransform = "capitalize"; row.appendChild(keyCell); row.appendChild(descCell); return row; }); rows.forEach((row) => table.appendChild(row)); const closeBtn = document.createElement("button"); closeBtn.textContent = "Close"; closeBtn.style.marginTop = "16px"; closeBtn.style.padding = "8px 16px"; closeBtn.style.border = "none"; closeBtn.style.background = "#cc0000"; closeBtn.style.color = "white"; closeBtn.style.borderRadius = "4px"; closeBtn.style.cursor = "pointer"; closeBtn.onclick = () => overlay.remove(); modal.appendChild(title); modal.appendChild(table); modal.appendChild(closeBtn); overlay.appendChild(modal); document.body.appendChild(overlay); } // Shows a brief notification to the user function showNotification(message, duration = 2000) { const notification = document.createElement("div"); notification.textContent = message; notification.style.position = "fixed"; notification.style.top = "20px"; notification.style.left = "50%"; notification.style.transform = "translateX(-50%)"; notification.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; notification.style.color = "white"; notification.style.padding = "10px 20px"; notification.style.borderRadius = "4px"; notification.style.zIndex = "9999"; notification.style.fontFamily = "Arial, sans-serif"; notification.style.textAlign = "center"; notification.style.maxWidth = "80%"; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = "0"; notification.style.transition = "opacity 0.5s ease"; setTimeout(() => document.body.removeChild(notification), 500); }, duration); } // Hotkey mappings with functions - Updated per user preference const HOTKEYS = { h: navigateToHome, // i -> h for home s: navigateToSubscriptions, // i -> s for subscriptions e: navigateToHistory, // i -> e for history w: navigateToWatchLater, // i -> w for watch later l: navigateToLikedVideos, // i -> l for liked videos t: navigateToTrending, // i -> t for trending L: navigateToLibrary, // i -> L (capital) for library y: copyVideoUrlWithTimestamp, // i -> y for copy URL with timestamp v: navigateToChannelVideos, // i -> a for channel videos q: navigateToChannelPlaylists, // i -> q for channel playlists n: navigateToNextVideo, // i -> n for next video p: navigateToPreviousVideo, // i -> p for previous video Tab: toggleSidebar, // i -> Tab for toggle sidebar S: triggerSaveButton, // i -> s (capital) for Save to playlist popup Y: copyShortenedUrl, // i -> Y (capital) for shortened URL C: navigateToCommunityTab, // i -> C (capital) for community tab "?": showHelpModal, }; // State variables let leaderPressed = false; let leaderTimer = null; // Function to handle keydown events function handleKeyDown(event) { // Check if user is typing in an input field if (isInputField(event.target)) { return; } // Get the key that was pressed (preserve case) const key = event.key; // If leader key is pressed if (key.toLowerCase() === LEADER_KEY) { // Prevent default action (like mini player) event.preventDefault(); event.stopPropagation(); // Set the leader state leaderPressed = true; // Clear any existing timer if (leaderTimer) { clearTimeout(leaderTimer); } // Set a timeout to reset the leader state leaderTimer = setTimeout(() => { leaderPressed = false; }, TIMEOUT); return; } // If a key is pressed after the leader key if (leaderPressed && HOTKEYS[key]) { // Prevent default action event.preventDefault(); event.stopPropagation(); // Execute the function associated with the key HOTKEYS[key](); // Reset leader state leaderPressed = false; clearTimeout(leaderTimer); } } // Helper function to check if the active element is an input field function isInputField(element) { const tagName = element.tagName.toLowerCase(); const type = element.type ? element.type.toLowerCase() : ""; return ( (tagName === "input" && (type === "text" || type === "password" || type === "email" || type === "number" || type === "search" || type === "tel" || type === "url")) || tagName === "textarea" || element.isContentEditable ); } // Add event listener for keydown document.addEventListener("keydown", handleKeyDown, true); // Show initial notification about the leader key on first load const firstRun = GM_getValue("firstRun", true); if (firstRun) { setTimeout(() => { showNotification( `YouTube Leader Key Navigation activated! Leader key is '${LEADER_KEY}'`, 5000, ); GM_setValue("firstRun", false); }, 2000); } // Logging for debugging console.log( `YouTube Leader Key Navigation loaded with leader key '${LEADER_KEY}'`, ); })();