// ==UserScript== // @name Global Slash Search & Vim Scroll (Pro Max) // @namespace http://quic.search/ // @version 6.0 // @description '/'搜索, '?'清空, 'Esc'退出, 'j/k'滚动, 'c'评论区, 'd'发弹幕 // @author Min9yu3 // @match *://*/* // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/566545/Global%20Slash%20Search%20%20Vim%20Scroll%20%28Pro%20Max%29.user.js // @updateURL https://update.greasyfork.icu/scripts/566545/Global%20Slash%20Search%20%20Vim%20Scroll%20%28Pro%20Max%29.meta.js // ==/UserScript== (function() { 'use strict'; // ========================================== // 【核心架构】:站点精准定位策略配置 // ========================================== const siteStrategies = { 'bilibili.com': { wrapper: 'bili-comments', getCommentInput: () => { const commentsEl = document.querySelector('bili-comments'); if (commentsEl && commentsEl.shadowRoot) { const sendEl = commentsEl.shadowRoot.querySelector('bili-comment-send'); if (sendEl && sendEl.shadowRoot) { return sendEl.shadowRoot.querySelector('textarea'); } } return document.querySelector('.reply-box-textarea, .reply-box textarea'); }, // 新增:B 站专属的弹幕输入框获取策略 getDanmakuInput: () => { // .bpx-player-dm-input 是点播视频的弹幕框 // .chat-input 是 B 站直播间的弹幕/聊天框 return document.querySelector('.bpx-player-dm-input, .chat-input'); } }, 'youtube.com': { wrapper: '#comments', getCommentInput: () => document.querySelector('#contenteditable-root'), // YouTube 直播间的聊天框 getDanmakuInput: () => document.querySelector('#chat-messages #input') }, 'default': { wrapper: '#comment, #comments, .comment-wrapper', getCommentInput: () => document.querySelector('textarea[placeholder*="评论"], textarea[placeholder*="comment" i]'), // 泛用型弹幕/聊天框匹配 getDanmakuInput: () => document.querySelector('input[placeholder*="弹幕"], input[class*="danmaku" i], .chat-input') } }; function getCurrentStrategy() { const host = window.location.hostname; for (const site in siteStrategies) { if (site !== 'default' && host.includes(site)) { return siteStrategies[site]; } } return siteStrategies['default']; } document.addEventListener('keydown', function(e) { const active = document.activeElement; const isInputActive = active && ( active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable ); // 【功能 2】:按 Esc 键退出聚焦,并模拟点击关闭面板 if (isInputActive && e.key === 'Escape') { setTimeout(() => { const mousedown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }); const mouseup = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }); document.body.dispatchEvent(mousedown); document.body.dispatchEvent(mouseup); const click = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); document.body.dispatchEvent(click); active.blur(); }, 20); return; } if (isInputActive) return; // 【功能 4】:j / k 网页上下顺滑滚动 if (!e.metaKey && !e.ctrlKey && !e.altKey) { if (e.key === 'j') { e.preventDefault(); window.scrollBy({ top: 150, left: 0, behavior: 'smooth' }); return; } if (e.key === 'k') { e.preventDefault(); window.scrollBy({ top: -150, left: 0, behavior: 'smooth' }); return; } } // ========================================== // 【新增功能 6】:'d' 键直达弹幕输入框 (Danmaku) // ========================================== if (e.key === 'c' || e.key === 'C') { e.preventDefault(); e.stopPropagation(); const strategy = getCurrentStrategy(); if (strategy.getDanmakuInput) { const danmakuInput = strategy.getDanmakuInput(); if (danmakuInput) { // 如果你当前正在看底部的评论,按 d 会先平滑滚回上方的播放器区域 // danmakuInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 延迟一点聚焦,等待滚动动画完成 setTimeout(() => { danmakuInput.focus(); }, 3); } else { console.log("[Global Slash Search] 当前页面未找到弹幕输入框。"); } } return; } // 【功能 5】:'c' 键精准定位并聚焦评论区 // 【功能 1 & 3】:'/' 聚焦全选,'?' 聚焦清空 if (e.key === '/' || e.key === '?') { e.preventDefault(); e.stopPropagation(); const searchSelectors = [ 'input[type="search"]', 'input[name="q"]', 'input[name="search"]', 'input[placeholder*="Search" i]', 'input[placeholder*="搜索"]', '.nav-search-input' ]; let searchInput = null; for (const selector of searchSelectors) { const candidates = document.querySelectorAll(selector); for (const input of candidates) { if (input.offsetParent !== null && !input.disabled && input.type !== 'hidden') { searchInput = input; break; } } if (searchInput) break; } if (searchInput) { searchInput.focus(); if (e.key === '?') { searchInput.value = ''; } else { searchInput.select(); } } } }, true); })();