您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filter videos by channel/keyword, with dropdown "Block Channel" button and menu commands to manage filter lists, plus import/export functionality.
// ==UserScript== // @name DEOVRContentFilter // @namespace http://tampermonkey.net/ // @version 1.0 // @description Filter videos by channel/keyword, with dropdown "Block Channel" button and menu commands to manage filter lists, plus import/export functionality. // @author Twine1481 // @match https://deovr.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=deovr.com // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // ==/UserScript== (function() { "use strict"; const SCRIPT_PREFIX = "[DEOVRContentFilter]"; console.log(`${SCRIPT_PREFIX} Script starting up...`); // ------------------------------------------------------------------------- // 1) CONFIGURATION // ------------------------------------------------------------------------- // Storage keys for persistence const STORAGE = { CHANNELS: "filteredChannels", KEYWORDS: "filteredKeywords" }; // Default filter lists (content-agnostic) const DEFAULT_CONFIG = { // Add your default filtered channels here filteredChannels: [ // Empty by default ], // Add your default filtered keywords here filteredKeywords: [ // Empty by default ] }; // ------------------------------------------------------------------------- // 2) STATE MANAGEMENT // ------------------------------------------------------------------------- /** * State management object for filter lists */ const FilterState = { // Current filter lists filteredChannels: [], filteredKeywords: [], /** * Initialize filter state from storage */ initialize() { // Load stored channels const storedChannels = GM_getValue(STORAGE.CHANNELS); const normalizedStoredChannels = Array.isArray(storedChannels) ? storedChannels.map(ch => ch.toLowerCase()) : []; // Load stored keywords const storedKeywords = GM_getValue(STORAGE.KEYWORDS); const normalizedStoredKeywords = Array.isArray(storedKeywords) ? storedKeywords.map(kw => kw.toLowerCase()) : []; // Merge default with stored values (removing duplicates) this.filteredChannels = [...new Set([ ...DEFAULT_CONFIG.filteredChannels.map(ch => ch.toLowerCase()), ...normalizedStoredChannels ])]; this.filteredKeywords = [...new Set([ ...DEFAULT_CONFIG.filteredKeywords.map(kw => kw.toLowerCase()), ...normalizedStoredKeywords ])]; // Log state this._logState(); }, /** * Add a channel to the filter list * @param {string} channel - Channel name to add * @returns {boolean} - Whether the channel was added */ addChannel(channel) { if (!channel || typeof channel !== 'string') return false; const normalizedChannel = channel.toLowerCase().trim(); if (!normalizedChannel) return false; if (this.filteredChannels.includes(normalizedChannel)) { return false; // Already in the list } this.filteredChannels.push(normalizedChannel); this._persistChannels(); return true; }, /** * Remove a channel from the filter list * @param {string} channel - Channel name to remove * @returns {boolean} - Whether the channel was removed */ removeChannel(channel) { if (!channel || typeof channel !== 'string') return false; const normalizedChannel = channel.toLowerCase().trim(); if (!normalizedChannel) return false; const initialLength = this.filteredChannels.length; this.filteredChannels = this.filteredChannels.filter(ch => ch !== normalizedChannel); if (this.filteredChannels.length !== initialLength) { this._persistChannels(); return true; } return false; }, /** * Add a keyword to the filter list * @param {string} keyword - Keyword to add * @returns {boolean} - Whether the keyword was added */ addKeyword(keyword) { if (!keyword || typeof keyword !== 'string') return false; const normalizedKeyword = keyword.toLowerCase().trim(); if (!normalizedKeyword) return false; if (this.filteredKeywords.includes(normalizedKeyword)) { return false; // Already in the list } this.filteredKeywords.push(normalizedKeyword); this._persistKeywords(); return true; }, /** * Remove a keyword from the filter list * @param {string} keyword - Keyword to remove * @returns {boolean} - Whether the keyword was removed */ removeKeyword(keyword) { if (!keyword || typeof keyword !== 'string') return false; const normalizedKeyword = keyword.toLowerCase().trim(); if (!normalizedKeyword) return false; const initialLength = this.filteredKeywords.length; this.filteredKeywords = this.filteredKeywords.filter(kw => kw !== normalizedKeyword); if (this.filteredKeywords.length !== initialLength) { this._persistKeywords(); return true; } return false; }, /** * Check if content should be filtered based on current filters * @param {string} text - Text content to check * @returns {Object} - Result with match details */ shouldFilter(text) { if (!text || typeof text !== 'string') { return { shouldFilter: false }; } const normalizedText = text.toLowerCase(); // Check for channel match const channelMatch = this.filteredChannels.some(channel => normalizedText.includes(channel)); // Check for keyword match const keywordMatch = this.filteredKeywords.some(keyword => normalizedText.includes(keyword)); return { shouldFilter: channelMatch || keywordMatch, channelMatch, keywordMatch }; }, /** * Persist channels to storage * @private */ _persistChannels() { GM_setValue(STORAGE.CHANNELS, this.filteredChannels); }, /** * Persist keywords to storage * @private */ _persistKeywords() { GM_setValue(STORAGE.KEYWORDS, this.filteredKeywords); }, /** * Log current state to console * @private */ _logState() { console.log(`${SCRIPT_PREFIX} Filtered Channels:`, this.filteredChannels); console.log(`${SCRIPT_PREFIX} Filtered Keywords:`, this.filteredKeywords); } }; // ------------------------------------------------------------------------- // 3) DOM INTERACTION // ------------------------------------------------------------------------- /** * DOM utilities for filtering content and UI modifications */ const DOMManager = { /** * Filter videos based on current filter state */ filterContent() { console.log(`${SCRIPT_PREFIX} Filtering content...`); const articles = document.querySelectorAll("article"); console.log(`${SCRIPT_PREFIX} Found ${articles.length} items to check.`); let filteredCount = 0; articles.forEach(article => { const articleText = article.textContent; const result = FilterState.shouldFilter(articleText); if (result.shouldFilter) { article.style.display = "none"; filteredCount++; console.log( `${SCRIPT_PREFIX} Filtered item:`, article, `channelMatch=${result.channelMatch}`, `keywordMatch=${result.keywordMatch}` ); } }); console.log(`${SCRIPT_PREFIX} Filtered ${filteredCount} of ${articles.length} items.`); }, /** * Inject "Block Channel" button into dropdown menu * @param {HTMLElement} dropdownMenu - The dropdown menu element */ injectBlockButton(dropdownMenu) { // Avoid duplicates if (dropdownMenu.querySelector(".filter-option")) return; const listItem = document.createElement("li"); listItem.classList.add("filter-option"); listItem.innerHTML = ` <div class="u-cursor--pointer u-fs--fo u-p--four u-lh--one u-block u-nowrap u-transition--base js-m-dropdown" data-qa="block-channel" style="display: flex; align-items: center;"> <span class="o-icon u-mr--three u-dg" style="width:18px;"> <svg class="o-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"></path> </svg> </span> <span class="u-inline-block u-align-y--m u-mr--four u-ug u-fw--semibold u-uppercase hover:u-bl">Block Channel</span> </div> `; // Add click handler listItem.addEventListener("click", () => this._handleBlockChannelClick(dropdownMenu)); // Add to dropdown dropdownMenu.appendChild(listItem); console.log(`${SCRIPT_PREFIX} Block button added to dropdown menu:`, dropdownMenu); }, /** * Scan existing dropdowns for adding block buttons */ scanExistingDropdowns() { console.log(`${SCRIPT_PREFIX} Scanning for existing dropdown menus...`); const dropdowns = document.querySelectorAll( "#content .m-dropdown.m-dropdown--grid-item .m-dropdown-content ul.u-list.u-l" ); console.log(`${SCRIPT_PREFIX} Found ${dropdowns.length} existing dropdown menu(s).`); dropdowns.forEach(dropdown => this.injectBlockButton(dropdown)); }, /** * Set up observer for dynamically added dropdowns */ setupDynamicObserver() { const contentElement = document.querySelector("#content") || document.body; const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { // Check if the added node is a dropdown menu if (node.matches && node.matches(".m-dropdown-content ul.u-list.u-l")) { this.injectBlockButton(node); } else { // Check for dropdown menus within the added node const nestedDropdowns = node.querySelectorAll ? node.querySelectorAll(".m-dropdown-content ul.u-list.u-l") : []; nestedDropdowns.forEach(dropdown => this.injectBlockButton(dropdown)); } } }); }); }); observer.observe(contentElement, { childList: true, subtree: true }); console.log(`${SCRIPT_PREFIX} Dynamic observer set up for new dropdown menus.`); }, /** * Handle click on "Block Channel" button * @private * @param {HTMLElement} dropdownMenu - The dropdown menu element */ _handleBlockChannelClick(dropdownMenu) { console.log(`${SCRIPT_PREFIX} Block Channel button clicked.`); // Find parent article const article = dropdownMenu.closest("article"); if (!article) { console.warn(`${SCRIPT_PREFIX} Could not find parent article.`); return; } // Find channel link const channelLink = article.querySelector('a[href^="/channel/"]'); if (!channelLink) { console.warn(`${SCRIPT_PREFIX} No channel link found.`); return; } // Get channel name let channelName = channelLink.dataset.amplitudePropsChannel; if (!channelName || !channelName.trim()) { channelName = channelLink.textContent.trim(); } channelName = channelName.toLowerCase(); // Validate channel name if (channelName.length < 2) { if (!confirm(`Channel name is "${channelName}" (very short). Block anyway?`)) { return; } } // Add to filter list if (FilterState.addChannel(channelName)) { alert(`Channel "${channelName}" added to block list.\nFiltering matching content...`); this.filterContent(); } else { alert(`Channel "${channelName}" is already blocked.`); } } }; // ------------------------------------------------------------------------- // 4) USER INTERFACE - MENU COMMANDS // ------------------------------------------------------------------------- /** * User interface for managing filter lists */ const UserInterface = { /** * Register all Tampermonkey menu commands */ registerMenuCommands() { GM_registerMenuCommand("Add Blocked Channel", () => this.addChannel()); GM_registerMenuCommand("Remove Blocked Channel", () => this.removeChannel()); GM_registerMenuCommand("Add Blocked Keyword", () => this.addKeyword()); GM_registerMenuCommand("Remove Blocked Keyword", () => this.removeKeyword()); GM_registerMenuCommand("Export Filter Lists", () => this.exportFilters()); GM_registerMenuCommand("Import Filter Lists", () => this.importFilters()); console.log(`${SCRIPT_PREFIX} Menu commands registered.`); }, /** * Prompt user to add a channel to the filter list */ addChannel() { const newChannel = prompt("Enter channel name to block:").trim(); if (!newChannel) return; if (FilterState.addChannel(newChannel)) { alert(`Channel "${newChannel}" added. Reload the page to update.`); } else { alert(`Channel "${newChannel}" is already blocked.`); } }, /** * Prompt user to remove a channel from the filter list */ removeChannel() { if (FilterState.filteredChannels.length === 0) { alert("No blocked channels."); return; } const listStr = FilterState.filteredChannels.join("\n"); const toRemove = prompt("Blocked channels:\n" + listStr + "\n\nEnter channel name to remove:"); if (!toRemove) return; if (FilterState.removeChannel(toRemove)) { alert(`Channel "${toRemove}" removed. Reload the page to update.`); } else { alert(`Channel "${toRemove}" not found.`); } }, /** * Prompt user to add a keyword to the filter list */ addKeyword() { const newKeyword = prompt("Enter a keyword to block:").trim(); if (!newKeyword) return; if (FilterState.addKeyword(newKeyword)) { alert(`Keyword "${newKeyword}" added. Reload the page to update.`); } else { alert(`Keyword "${newKeyword}" is already blocked.`); } }, /** * Prompt user to remove a keyword from the filter list */ removeKeyword() { if (FilterState.filteredKeywords.length === 0) { alert("No blocked keywords."); return; } const listStr = FilterState.filteredKeywords.join("\n"); const toRemove = prompt("Blocked keywords:\n" + listStr + "\n\nEnter keyword to remove:"); if (!toRemove) return; if (FilterState.removeKeyword(toRemove)) { alert(`Keyword "${toRemove}" removed. Reload the page to update.`); } else { alert(`Keyword "${toRemove}" not found.`); } }, /** * Export filter lists to JSON file */ exportFilters() { const data = { filteredChannels: FilterState.filteredChannels, filteredKeywords: FilterState.filteredKeywords }; const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = url; downloadLink.download = "deovr_filter_lists.json"; downloadLink.click(); URL.revokeObjectURL(url); }, /** * Import filter lists from JSON file * Uses a modal dialog approach to avoid browser security restrictions */ importFilters() { console.log(`${SCRIPT_PREFIX} Starting import process with modal dialog...`); // Create modal container const modalOverlay = document.createElement('div'); modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 9999; display: flex; justify-content: center; align-items: center; `; // Create modal dialog const modalContent = document.createElement('div'); modalContent.style.cssText = ` background-color: white; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); `; // Create heading const heading = document.createElement('h2'); heading.textContent = 'Import Filter Lists'; heading.style.cssText = ` margin-top: 0; margin-bottom: 15px; font-size: 18px; `; // Create description const description = document.createElement('p'); description.textContent = 'Select a JSON file containing filter lists.'; description.style.marginBottom = '20px'; // Create file input const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'application/json'; fileInput.style.display = 'block'; fileInput.style.marginBottom = '15px'; fileInput.style.width = '100%'; // Create buttons container const buttonsContainer = document.createElement('div'); buttonsContainer.style.cssText = ` display: flex; justify-content: flex-end; gap: 10px; `; // Create cancel button const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.style.cssText = ` padding: 8px 16px; background-color: #f1f1f1; border: none; border-radius: 4px; cursor: pointer; `; // Create import button const importButton = document.createElement('button'); importButton.textContent = 'Import'; importButton.style.cssText = ` padding: 8px 16px; background-color: #4285f4; color: white; border: none; border-radius: 4px; cursor: pointer; `; importButton.disabled = true; // Add event listener to enable import button when file is selected fileInput.addEventListener('change', () => { importButton.disabled = !fileInput.files || fileInput.files.length === 0; }); // Add cancel button functionality cancelButton.addEventListener('click', () => { document.body.removeChild(modalOverlay); }); // Add import button functionality importButton.addEventListener('click', () => { const file = fileInput.files[0]; if (!file) return; console.log(`${SCRIPT_PREFIX} Reading file: ${file.name}`); const reader = new FileReader(); // Handle file reading errors reader.onerror = error => { console.error(`${SCRIPT_PREFIX} Error reading file:`, error); alert(`Error reading file: ${error.message || "Unknown error"}`); document.body.removeChild(modalOverlay); }; // Process file contents when loaded reader.onload = e => { console.log(`${SCRIPT_PREFIX} File read complete, processing content...`); try { const imported = JSON.parse(e.target.result); console.log(`${SCRIPT_PREFIX} Parsed JSON:`, imported); let updated = false; // Support both new and old property names for backward compatibility // Check for channels (new property name) if (imported.filteredChannels && Array.isArray(imported.filteredChannels)) { console.log(`${SCRIPT_PREFIX} Importing filtered channels:`, imported.filteredChannels); FilterState.filteredChannels = imported.filteredChannels.map(ch => ch.toLowerCase()); FilterState._persistChannels(); updated = true; } // Check for channels (old property name from original script) else if (imported.blockedChannels && Array.isArray(imported.blockedChannels)) { console.log(`${SCRIPT_PREFIX} Importing blocked channels (legacy format):`, imported.blockedChannels); FilterState.filteredChannels = imported.blockedChannels.map(ch => ch.toLowerCase()); FilterState._persistChannels(); updated = true; } // Check for keywords (new property name) if (imported.filteredKeywords && Array.isArray(imported.filteredKeywords)) { console.log(`${SCRIPT_PREFIX} Importing filtered keywords:`, imported.filteredKeywords); FilterState.filteredKeywords = imported.filteredKeywords.map(kw => kw.toLowerCase()); FilterState._persistKeywords(); updated = true; } // Check for keywords (old property name from original script) else if (imported.blockedWords && Array.isArray(imported.blockedWords)) { console.log(`${SCRIPT_PREFIX} Importing blocked words (legacy format):`, imported.blockedWords); FilterState.filteredKeywords = imported.blockedWords.map(kw => kw.toLowerCase()); FilterState._persistKeywords(); updated = true; } document.body.removeChild(modalOverlay); if (updated) { alert("Filter lists imported successfully. Reload the page to update."); } else { alert("No valid filter lists found in the imported file."); } } catch (error) { console.error(`${SCRIPT_PREFIX} JSON parsing error:`, error); alert(`Error importing JSON: ${error.message}`); document.body.removeChild(modalOverlay); } }; // Start reading the file as text reader.readAsText(file); }); // Assemble the modal buttonsContainer.appendChild(cancelButton); buttonsContainer.appendChild(importButton); modalContent.appendChild(heading); modalContent.appendChild(description); modalContent.appendChild(fileInput); modalContent.appendChild(buttonsContainer); modalOverlay.appendChild(modalContent); // Add modal to the page document.body.appendChild(modalOverlay); // Add click handler to close when clicking outside the modal modalOverlay.addEventListener('click', (event) => { if (event.target === modalOverlay) { document.body.removeChild(modalOverlay); } }); } }; // ------------------------------------------------------------------------- // 5) INITIALIZATION // ------------------------------------------------------------------------- /** * Initialize the script */ function initialize() { // Initialize state FilterState.initialize(); // Register menu commands UserInterface.registerMenuCommands(); // Initial content filtering DOMManager.filterContent(); // Set up UI DOMManager.scanExistingDropdowns(); DOMManager.setupDynamicObserver(); console.log(`${SCRIPT_PREFIX} Script fully initialized!`); } // Start the script initialize(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址