DEOVRContentFilter

Filter videos by channel/keyword, with dropdown "Block Channel" button and menu commands to manage filter lists, plus import/export functionality.

目前为 2025-03-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name DEOVRContentFilter
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Filter videos by channel/keyword, with dropdown "Block Channel" button and menu commands to manage filter lists, plus import/export functionality.
  6. // @author Twine1481
  7. // @match https://deovr.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=deovr.com
  9. // @grant unsafeWindow
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_registerMenuCommand
  13. // @run-at document-idle
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. "use strict";
  19.  
  20. const SCRIPT_PREFIX = "[DEOVRContentFilter]";
  21. console.log(`${SCRIPT_PREFIX} Script starting up...`);
  22.  
  23. // -------------------------------------------------------------------------
  24. // 1) CONFIGURATION
  25. // -------------------------------------------------------------------------
  26.  
  27. // Storage keys for persistence
  28. const STORAGE = {
  29. CHANNELS: "filteredChannels",
  30. KEYWORDS: "filteredKeywords"
  31. };
  32.  
  33. // Default filter lists (content-agnostic)
  34. const DEFAULT_CONFIG = {
  35. // Add your default filtered channels here
  36. filteredChannels: [
  37. // Empty by default
  38. ],
  39.  
  40. // Add your default filtered keywords here
  41. filteredKeywords: [
  42. // Empty by default
  43. ]
  44. };
  45.  
  46. // -------------------------------------------------------------------------
  47. // 2) STATE MANAGEMENT
  48. // -------------------------------------------------------------------------
  49.  
  50. /**
  51. * State management object for filter lists
  52. */
  53. const FilterState = {
  54. // Current filter lists
  55. filteredChannels: [],
  56. filteredKeywords: [],
  57.  
  58. /**
  59. * Initialize filter state from storage
  60. */
  61. initialize() {
  62. // Load stored channels
  63. const storedChannels = GM_getValue(STORAGE.CHANNELS);
  64. const normalizedStoredChannels = Array.isArray(storedChannels)
  65. ? storedChannels.map(ch => ch.toLowerCase())
  66. : [];
  67.  
  68. // Load stored keywords
  69. const storedKeywords = GM_getValue(STORAGE.KEYWORDS);
  70. const normalizedStoredKeywords = Array.isArray(storedKeywords)
  71. ? storedKeywords.map(kw => kw.toLowerCase())
  72. : [];
  73.  
  74. // Merge default with stored values (removing duplicates)
  75. this.filteredChannels = [...new Set([
  76. ...DEFAULT_CONFIG.filteredChannels.map(ch => ch.toLowerCase()),
  77. ...normalizedStoredChannels
  78. ])];
  79.  
  80. this.filteredKeywords = [...new Set([
  81. ...DEFAULT_CONFIG.filteredKeywords.map(kw => kw.toLowerCase()),
  82. ...normalizedStoredKeywords
  83. ])];
  84.  
  85. // Log state
  86. this._logState();
  87. },
  88.  
  89. /**
  90. * Add a channel to the filter list
  91. * @param {string} channel - Channel name to add
  92. * @returns {boolean} - Whether the channel was added
  93. */
  94. addChannel(channel) {
  95. if (!channel || typeof channel !== 'string') return false;
  96.  
  97. const normalizedChannel = channel.toLowerCase().trim();
  98. if (!normalizedChannel) return false;
  99.  
  100. if (this.filteredChannels.includes(normalizedChannel)) {
  101. return false; // Already in the list
  102. }
  103.  
  104. this.filteredChannels.push(normalizedChannel);
  105. this._persistChannels();
  106. return true;
  107. },
  108.  
  109. /**
  110. * Remove a channel from the filter list
  111. * @param {string} channel - Channel name to remove
  112. * @returns {boolean} - Whether the channel was removed
  113. */
  114. removeChannel(channel) {
  115. if (!channel || typeof channel !== 'string') return false;
  116.  
  117. const normalizedChannel = channel.toLowerCase().trim();
  118. if (!normalizedChannel) return false;
  119.  
  120. const initialLength = this.filteredChannels.length;
  121. this.filteredChannels = this.filteredChannels.filter(ch => ch !== normalizedChannel);
  122.  
  123. if (this.filteredChannels.length !== initialLength) {
  124. this._persistChannels();
  125. return true;
  126. }
  127.  
  128. return false;
  129. },
  130.  
  131. /**
  132. * Add a keyword to the filter list
  133. * @param {string} keyword - Keyword to add
  134. * @returns {boolean} - Whether the keyword was added
  135. */
  136. addKeyword(keyword) {
  137. if (!keyword || typeof keyword !== 'string') return false;
  138.  
  139. const normalizedKeyword = keyword.toLowerCase().trim();
  140. if (!normalizedKeyword) return false;
  141.  
  142. if (this.filteredKeywords.includes(normalizedKeyword)) {
  143. return false; // Already in the list
  144. }
  145.  
  146. this.filteredKeywords.push(normalizedKeyword);
  147. this._persistKeywords();
  148. return true;
  149. },
  150.  
  151. /**
  152. * Remove a keyword from the filter list
  153. * @param {string} keyword - Keyword to remove
  154. * @returns {boolean} - Whether the keyword was removed
  155. */
  156. removeKeyword(keyword) {
  157. if (!keyword || typeof keyword !== 'string') return false;
  158.  
  159. const normalizedKeyword = keyword.toLowerCase().trim();
  160. if (!normalizedKeyword) return false;
  161.  
  162. const initialLength = this.filteredKeywords.length;
  163. this.filteredKeywords = this.filteredKeywords.filter(kw => kw !== normalizedKeyword);
  164.  
  165. if (this.filteredKeywords.length !== initialLength) {
  166. this._persistKeywords();
  167. return true;
  168. }
  169.  
  170. return false;
  171. },
  172.  
  173. /**
  174. * Check if content should be filtered based on current filters
  175. * @param {string} text - Text content to check
  176. * @returns {Object} - Result with match details
  177. */
  178. shouldFilter(text) {
  179. if (!text || typeof text !== 'string') {
  180. return { shouldFilter: false };
  181. }
  182.  
  183. const normalizedText = text.toLowerCase();
  184.  
  185. // Check for channel match
  186. const channelMatch = this.filteredChannels.some(channel =>
  187. normalizedText.includes(channel));
  188.  
  189. // Check for keyword match
  190. const keywordMatch = this.filteredKeywords.some(keyword =>
  191. normalizedText.includes(keyword));
  192.  
  193. return {
  194. shouldFilter: channelMatch || keywordMatch,
  195. channelMatch,
  196. keywordMatch
  197. };
  198. },
  199.  
  200. /**
  201. * Persist channels to storage
  202. * @private
  203. */
  204. _persistChannels() {
  205. GM_setValue(STORAGE.CHANNELS, this.filteredChannels);
  206. },
  207.  
  208. /**
  209. * Persist keywords to storage
  210. * @private
  211. */
  212. _persistKeywords() {
  213. GM_setValue(STORAGE.KEYWORDS, this.filteredKeywords);
  214. },
  215.  
  216. /**
  217. * Log current state to console
  218. * @private
  219. */
  220. _logState() {
  221. console.log(`${SCRIPT_PREFIX} Filtered Channels:`, this.filteredChannels);
  222. console.log(`${SCRIPT_PREFIX} Filtered Keywords:`, this.filteredKeywords);
  223. }
  224. };
  225.  
  226. // -------------------------------------------------------------------------
  227. // 3) DOM INTERACTION
  228. // -------------------------------------------------------------------------
  229.  
  230. /**
  231. * DOM utilities for filtering content and UI modifications
  232. */
  233. const DOMManager = {
  234. /**
  235. * Filter videos based on current filter state
  236. */
  237. filterContent() {
  238. console.log(`${SCRIPT_PREFIX} Filtering content...`);
  239.  
  240. const articles = document.querySelectorAll("article");
  241. console.log(`${SCRIPT_PREFIX} Found ${articles.length} items to check.`);
  242.  
  243. let filteredCount = 0;
  244. articles.forEach(article => {
  245. const articleText = article.textContent;
  246. const result = FilterState.shouldFilter(articleText);
  247.  
  248. if (result.shouldFilter) {
  249. article.style.display = "none";
  250. filteredCount++;
  251.  
  252. console.log(
  253. `${SCRIPT_PREFIX} Filtered item:`,
  254. article,
  255. `channelMatch=${result.channelMatch}`,
  256. `keywordMatch=${result.keywordMatch}`
  257. );
  258. }
  259. });
  260.  
  261. console.log(`${SCRIPT_PREFIX} Filtered ${filteredCount} of ${articles.length} items.`);
  262. },
  263.  
  264. /**
  265. * Inject "Block Channel" button into dropdown menu
  266. * @param {HTMLElement} dropdownMenu - The dropdown menu element
  267. */
  268. injectBlockButton(dropdownMenu) {
  269. // Avoid duplicates
  270. if (dropdownMenu.querySelector(".filter-option")) return;
  271.  
  272. const listItem = document.createElement("li");
  273. listItem.classList.add("filter-option");
  274. listItem.innerHTML = `
  275. <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;">
  276. <span class="o-icon u-mr--three u-dg" style="width:18px;">
  277. <svg class="o-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;"
  278. viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  279. <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>
  280. </svg>
  281. </span>
  282. <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>
  283. </div>
  284. `;
  285.  
  286. // Add click handler
  287. listItem.addEventListener("click", () => this._handleBlockChannelClick(dropdownMenu));
  288.  
  289. // Add to dropdown
  290. dropdownMenu.appendChild(listItem);
  291. console.log(`${SCRIPT_PREFIX} Block button added to dropdown menu:`, dropdownMenu);
  292. },
  293.  
  294. /**
  295. * Scan existing dropdowns for adding block buttons
  296. */
  297. scanExistingDropdowns() {
  298. console.log(`${SCRIPT_PREFIX} Scanning for existing dropdown menus...`);
  299.  
  300. const dropdowns = document.querySelectorAll(
  301. "#content .m-dropdown.m-dropdown--grid-item .m-dropdown-content ul.u-list.u-l"
  302. );
  303.  
  304. console.log(`${SCRIPT_PREFIX} Found ${dropdowns.length} existing dropdown menu(s).`);
  305. dropdowns.forEach(dropdown => this.injectBlockButton(dropdown));
  306. },
  307.  
  308. /**
  309. * Set up observer for dynamically added dropdowns
  310. */
  311. setupDynamicObserver() {
  312. const contentElement = document.querySelector("#content") || document.body;
  313.  
  314. const observer = new MutationObserver(mutations => {
  315. mutations.forEach(mutation => {
  316. mutation.addedNodes.forEach(node => {
  317. if (node.nodeType === Node.ELEMENT_NODE) {
  318. // Check if the added node is a dropdown menu
  319. if (node.matches && node.matches(".m-dropdown-content ul.u-list.u-l")) {
  320. this.injectBlockButton(node);
  321. } else {
  322. // Check for dropdown menus within the added node
  323. const nestedDropdowns = node.querySelectorAll ?
  324. node.querySelectorAll(".m-dropdown-content ul.u-list.u-l") :
  325. [];
  326.  
  327. nestedDropdowns.forEach(dropdown => this.injectBlockButton(dropdown));
  328. }
  329. }
  330. });
  331. });
  332. });
  333.  
  334. observer.observe(contentElement, { childList: true, subtree: true });
  335. console.log(`${SCRIPT_PREFIX} Dynamic observer set up for new dropdown menus.`);
  336. },
  337.  
  338. /**
  339. * Handle click on "Block Channel" button
  340. * @private
  341. * @param {HTMLElement} dropdownMenu - The dropdown menu element
  342. */
  343. _handleBlockChannelClick(dropdownMenu) {
  344. console.log(`${SCRIPT_PREFIX} Block Channel button clicked.`);
  345.  
  346. // Find parent article
  347. const article = dropdownMenu.closest("article");
  348. if (!article) {
  349. console.warn(`${SCRIPT_PREFIX} Could not find parent article.`);
  350. return;
  351. }
  352.  
  353. // Find channel link
  354. const channelLink = article.querySelector('a[href^="/channel/"]');
  355. if (!channelLink) {
  356. console.warn(`${SCRIPT_PREFIX} No channel link found.`);
  357. return;
  358. }
  359.  
  360. // Get channel name
  361. let channelName = channelLink.dataset.amplitudePropsChannel;
  362. if (!channelName || !channelName.trim()) {
  363. channelName = channelLink.textContent.trim();
  364. }
  365. channelName = channelName.toLowerCase();
  366.  
  367. // Validate channel name
  368. if (channelName.length < 2) {
  369. if (!confirm(`Channel name is "${channelName}" (very short). Block anyway?`)) {
  370. return;
  371. }
  372. }
  373.  
  374. // Add to filter list
  375. if (FilterState.addChannel(channelName)) {
  376. alert(`Channel "${channelName}" added to block list.\nFiltering matching content...`);
  377. this.filterContent();
  378. } else {
  379. alert(`Channel "${channelName}" is already blocked.`);
  380. }
  381. }
  382. };
  383.  
  384. // -------------------------------------------------------------------------
  385. // 4) USER INTERFACE - MENU COMMANDS
  386. // -------------------------------------------------------------------------
  387.  
  388. /**
  389. * User interface for managing filter lists
  390. */
  391. const UserInterface = {
  392. /**
  393. * Register all Tampermonkey menu commands
  394. */
  395. registerMenuCommands() {
  396. GM_registerMenuCommand("Add Blocked Channel", () => this.addChannel());
  397. GM_registerMenuCommand("Remove Blocked Channel", () => this.removeChannel());
  398. GM_registerMenuCommand("Add Blocked Keyword", () => this.addKeyword());
  399. GM_registerMenuCommand("Remove Blocked Keyword", () => this.removeKeyword());
  400. GM_registerMenuCommand("Export Filter Lists", () => this.exportFilters());
  401. GM_registerMenuCommand("Import Filter Lists", () => this.importFilters());
  402.  
  403. console.log(`${SCRIPT_PREFIX} Menu commands registered.`);
  404. },
  405.  
  406. /**
  407. * Prompt user to add a channel to the filter list
  408. */
  409. addChannel() {
  410. const newChannel = prompt("Enter channel name to block:").trim();
  411. if (!newChannel) return;
  412.  
  413. if (FilterState.addChannel(newChannel)) {
  414. alert(`Channel "${newChannel}" added. Reload the page to update.`);
  415. } else {
  416. alert(`Channel "${newChannel}" is already blocked.`);
  417. }
  418. },
  419.  
  420. /**
  421. * Prompt user to remove a channel from the filter list
  422. */
  423. removeChannel() {
  424. if (FilterState.filteredChannels.length === 0) {
  425. alert("No blocked channels.");
  426. return;
  427. }
  428.  
  429. const listStr = FilterState.filteredChannels.join("\n");
  430. const toRemove = prompt("Blocked channels:\n" + listStr + "\n\nEnter channel name to remove:");
  431. if (!toRemove) return;
  432.  
  433. if (FilterState.removeChannel(toRemove)) {
  434. alert(`Channel "${toRemove}" removed. Reload the page to update.`);
  435. } else {
  436. alert(`Channel "${toRemove}" not found.`);
  437. }
  438. },
  439.  
  440. /**
  441. * Prompt user to add a keyword to the filter list
  442. */
  443. addKeyword() {
  444. const newKeyword = prompt("Enter a keyword to block:").trim();
  445. if (!newKeyword) return;
  446.  
  447. if (FilterState.addKeyword(newKeyword)) {
  448. alert(`Keyword "${newKeyword}" added. Reload the page to update.`);
  449. } else {
  450. alert(`Keyword "${newKeyword}" is already blocked.`);
  451. }
  452. },
  453.  
  454. /**
  455. * Prompt user to remove a keyword from the filter list
  456. */
  457. removeKeyword() {
  458. if (FilterState.filteredKeywords.length === 0) {
  459. alert("No blocked keywords.");
  460. return;
  461. }
  462.  
  463. const listStr = FilterState.filteredKeywords.join("\n");
  464. const toRemove = prompt("Blocked keywords:\n" + listStr + "\n\nEnter keyword to remove:");
  465. if (!toRemove) return;
  466.  
  467. if (FilterState.removeKeyword(toRemove)) {
  468. alert(`Keyword "${toRemove}" removed. Reload the page to update.`);
  469. } else {
  470. alert(`Keyword "${toRemove}" not found.`);
  471. }
  472. },
  473.  
  474. /**
  475. * Export filter lists to JSON file
  476. */
  477. exportFilters() {
  478. const data = {
  479. filteredChannels: FilterState.filteredChannels,
  480. filteredKeywords: FilterState.filteredKeywords
  481. };
  482.  
  483. const json = JSON.stringify(data, null, 2);
  484. const blob = new Blob([json], { type: "application/json" });
  485. const url = URL.createObjectURL(blob);
  486.  
  487. const downloadLink = document.createElement("a");
  488. downloadLink.href = url;
  489. downloadLink.download = "deovr_filter_lists.json";
  490. downloadLink.click();
  491.  
  492. URL.revokeObjectURL(url);
  493. },
  494.  
  495. /**
  496. * Import filter lists from JSON file
  497. * Uses a modal dialog approach to avoid browser security restrictions
  498. */
  499. importFilters() {
  500. console.log(`${SCRIPT_PREFIX} Starting import process with modal dialog...`);
  501.  
  502. // Create modal container
  503. const modalOverlay = document.createElement('div');
  504. modalOverlay.style.cssText = `
  505. position: fixed;
  506. top: 0;
  507. left: 0;
  508. width: 100%;
  509. height: 100%;
  510. background-color: rgba(0, 0, 0, 0.5);
  511. z-index: 9999;
  512. display: flex;
  513. justify-content: center;
  514. align-items: center;
  515. `;
  516.  
  517. // Create modal dialog
  518. const modalContent = document.createElement('div');
  519. modalContent.style.cssText = `
  520. background-color: white;
  521. padding: 20px;
  522. border-radius: 8px;
  523. max-width: 500px;
  524. width: 90%;
  525. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  526. `;
  527.  
  528. // Create heading
  529. const heading = document.createElement('h2');
  530. heading.textContent = 'Import Filter Lists';
  531. heading.style.cssText = `
  532. margin-top: 0;
  533. margin-bottom: 15px;
  534. font-size: 18px;
  535. `;
  536.  
  537. // Create description
  538. const description = document.createElement('p');
  539. description.textContent = 'Select a JSON file containing filter lists.';
  540. description.style.marginBottom = '20px';
  541.  
  542. // Create file input
  543. const fileInput = document.createElement('input');
  544. fileInput.type = 'file';
  545. fileInput.accept = 'application/json';
  546. fileInput.style.display = 'block';
  547. fileInput.style.marginBottom = '15px';
  548. fileInput.style.width = '100%';
  549.  
  550. // Create buttons container
  551. const buttonsContainer = document.createElement('div');
  552. buttonsContainer.style.cssText = `
  553. display: flex;
  554. justify-content: flex-end;
  555. gap: 10px;
  556. `;
  557.  
  558. // Create cancel button
  559. const cancelButton = document.createElement('button');
  560. cancelButton.textContent = 'Cancel';
  561. cancelButton.style.cssText = `
  562. padding: 8px 16px;
  563. background-color: #f1f1f1;
  564. border: none;
  565. border-radius: 4px;
  566. cursor: pointer;
  567. `;
  568.  
  569. // Create import button
  570. const importButton = document.createElement('button');
  571. importButton.textContent = 'Import';
  572. importButton.style.cssText = `
  573. padding: 8px 16px;
  574. background-color: #4285f4;
  575. color: white;
  576. border: none;
  577. border-radius: 4px;
  578. cursor: pointer;
  579. `;
  580. importButton.disabled = true;
  581.  
  582. // Add event listener to enable import button when file is selected
  583. fileInput.addEventListener('change', () => {
  584. importButton.disabled = !fileInput.files || fileInput.files.length === 0;
  585. });
  586.  
  587. // Add cancel button functionality
  588. cancelButton.addEventListener('click', () => {
  589. document.body.removeChild(modalOverlay);
  590. });
  591.  
  592. // Add import button functionality
  593. importButton.addEventListener('click', () => {
  594. const file = fileInput.files[0];
  595. if (!file) return;
  596.  
  597. console.log(`${SCRIPT_PREFIX} Reading file: ${file.name}`);
  598.  
  599. const reader = new FileReader();
  600.  
  601. // Handle file reading errors
  602. reader.onerror = error => {
  603. console.error(`${SCRIPT_PREFIX} Error reading file:`, error);
  604. alert(`Error reading file: ${error.message || "Unknown error"}`);
  605. document.body.removeChild(modalOverlay);
  606. };
  607.  
  608. // Process file contents when loaded
  609. reader.onload = e => {
  610. console.log(`${SCRIPT_PREFIX} File read complete, processing content...`);
  611.  
  612. try {
  613. const imported = JSON.parse(e.target.result);
  614. console.log(`${SCRIPT_PREFIX} Parsed JSON:`, imported);
  615.  
  616. let updated = false;
  617.  
  618. // Support both new and old property names for backward compatibility
  619.  
  620. // Check for channels (new property name)
  621. if (imported.filteredChannels && Array.isArray(imported.filteredChannels)) {
  622. console.log(`${SCRIPT_PREFIX} Importing filtered channels:`, imported.filteredChannels);
  623. FilterState.filteredChannels = imported.filteredChannels.map(ch => ch.toLowerCase());
  624. FilterState._persistChannels();
  625. updated = true;
  626. }
  627. // Check for channels (old property name from original script)
  628. else if (imported.blockedChannels && Array.isArray(imported.blockedChannels)) {
  629. console.log(`${SCRIPT_PREFIX} Importing blocked channels (legacy format):`, imported.blockedChannels);
  630. FilterState.filteredChannels = imported.blockedChannels.map(ch => ch.toLowerCase());
  631. FilterState._persistChannels();
  632. updated = true;
  633. }
  634.  
  635. // Check for keywords (new property name)
  636. if (imported.filteredKeywords && Array.isArray(imported.filteredKeywords)) {
  637. console.log(`${SCRIPT_PREFIX} Importing filtered keywords:`, imported.filteredKeywords);
  638. FilterState.filteredKeywords = imported.filteredKeywords.map(kw => kw.toLowerCase());
  639. FilterState._persistKeywords();
  640. updated = true;
  641. }
  642. // Check for keywords (old property name from original script)
  643. else if (imported.blockedWords && Array.isArray(imported.blockedWords)) {
  644. console.log(`${SCRIPT_PREFIX} Importing blocked words (legacy format):`, imported.blockedWords);
  645. FilterState.filteredKeywords = imported.blockedWords.map(kw => kw.toLowerCase());
  646. FilterState._persistKeywords();
  647. updated = true;
  648. }
  649.  
  650. document.body.removeChild(modalOverlay);
  651.  
  652. if (updated) {
  653. alert("Filter lists imported successfully. Reload the page to update.");
  654. } else {
  655. alert("No valid filter lists found in the imported file.");
  656. }
  657. } catch (error) {
  658. console.error(`${SCRIPT_PREFIX} JSON parsing error:`, error);
  659. alert(`Error importing JSON: ${error.message}`);
  660. document.body.removeChild(modalOverlay);
  661. }
  662. };
  663.  
  664. // Start reading the file as text
  665. reader.readAsText(file);
  666. });
  667.  
  668. // Assemble the modal
  669. buttonsContainer.appendChild(cancelButton);
  670. buttonsContainer.appendChild(importButton);
  671.  
  672. modalContent.appendChild(heading);
  673. modalContent.appendChild(description);
  674. modalContent.appendChild(fileInput);
  675. modalContent.appendChild(buttonsContainer);
  676.  
  677. modalOverlay.appendChild(modalContent);
  678.  
  679. // Add modal to the page
  680. document.body.appendChild(modalOverlay);
  681.  
  682. // Add click handler to close when clicking outside the modal
  683. modalOverlay.addEventListener('click', (event) => {
  684. if (event.target === modalOverlay) {
  685. document.body.removeChild(modalOverlay);
  686. }
  687. });
  688. }
  689. };
  690.  
  691. // -------------------------------------------------------------------------
  692. // 5) INITIALIZATION
  693. // -------------------------------------------------------------------------
  694.  
  695. /**
  696. * Initialize the script
  697. */
  698. function initialize() {
  699. // Initialize state
  700. FilterState.initialize();
  701.  
  702. // Register menu commands
  703. UserInterface.registerMenuCommands();
  704.  
  705. // Initial content filtering
  706. DOMManager.filterContent();
  707.  
  708. // Set up UI
  709. DOMManager.scanExistingDropdowns();
  710. DOMManager.setupDynamicObserver();
  711.  
  712. console.log(`${SCRIPT_PREFIX} Script fully initialized!`);
  713. }
  714.  
  715. // Start the script
  716. initialize();
  717. })();