From 82145184df63bb213cea53901eead186e28bb011 Mon Sep 17 00:00:00 2001 From: meowarex Date: Thu, 12 Jun 2025 03:19:44 +1000 Subject: [PATCH] WIP | Element Hider Plugin --- plugins/element-hider-luna/package.json | 11 + plugins/element-hider-luna/src/index.ts | 332 ++++++++++++++++++++++ plugins/element-hider-luna/src/styles.css | 63 ++++ 3 files changed, 406 insertions(+) create mode 100644 plugins/element-hider-luna/package.json create mode 100644 plugins/element-hider-luna/src/index.ts create mode 100644 plugins/element-hider-luna/src/styles.css diff --git a/plugins/element-hider-luna/package.json b/plugins/element-hider-luna/package.json new file mode 100644 index 0000000..7e04654 --- /dev/null +++ b/plugins/element-hider-luna/package.json @@ -0,0 +1,11 @@ +{ + "name": "@meowarex/element-hider", + "description": "adds a right-click option to hide any element on the page.", + "author": { + "name": "meowarex", + "url": "https://github.com/meowarex", + "avatarUrl": "https://avatars.githubusercontent.com/u/90243579" + }, + "main": "./src/index.ts", + "type": "module" +} \ No newline at end of file diff --git a/plugins/element-hider-luna/src/index.ts b/plugins/element-hider-luna/src/index.ts new file mode 100644 index 0000000..a8b658f --- /dev/null +++ b/plugins/element-hider-luna/src/index.ts @@ -0,0 +1,332 @@ +import { LunaUnload, Tracer } from "@luna/core"; +import { StyleTag, ContextMenu } from "@luna/lib"; + +// Import CSS directly using Luna's file:// syntax +import styles from "file://styles.css?minify"; + +export const { trace } = Tracer("[Element Hider]"); + +// Clean up resources +export const unloads = new Set(); + +// StyleTag for element hider styling +const styleTag = new StyleTag("Element-Hider", unloads, styles); + +// State management +let targetElement: HTMLElement | null = null; +let hiddenElements = new WeakSet(); +let hiddenElementsArray: HTMLElement[] = []; // Keep array for iteration since WeakSet isn't iterable + + + +// Hide element directly without animation (for restoration) +function hideElementDirectly(element: HTMLElement): void { + element.classList.add("element-hider-hidden"); + hiddenElements.add(element); + hiddenElementsArray.push(element); +} + +// Hide the target element with animation +function hideTargetElement(): void { + if (!targetElement) return; + + trace.msg.log(`Hiding element: ${targetElement.tagName}${targetElement.className ? '.' + targetElement.className.split(' ').join('.') : ''}`); + + // Add hiding animation class + targetElement.classList.add("element-hider-hiding"); + + // Store reference to the element + const elementToHide = targetElement; + + // Wait for animation to complete, then hide + setTimeout(() => { + elementToHide.classList.add("element-hider-hidden"); + elementToHide.classList.remove("element-hider-hiding", "element-hider-target"); + hiddenElements.add(elementToHide); + hiddenElementsArray.push(elementToHide); + }, 300); + + // Clear target reference + targetElement = null; +} + +// Show all hidden elements +function showAllElements(): void { + trace.msg.log(`Showing ${hiddenElementsArray.length} hidden elements`); + + // Use array to iterate and show all hidden elements + hiddenElementsArray.forEach(element => { + // Check if element is still in DOM + if (document.body.contains(element)) { + element.classList.remove("element-hider-hidden", "element-hider-hiding"); + } + }); + + // Clear both collections + hiddenElements = new WeakSet(); + hiddenElementsArray = []; +} + +// Handle highlighting target element on hover +function highlightElement(element: HTMLElement): void { + // Remove previous highlights + document.querySelectorAll('.element-hider-target').forEach(el => { + el.classList.remove('element-hider-target'); + }); + + // Highlight current element + element.classList.add('element-hider-target'); + targetElement = element; +} + +// Remove highlight +function removeHighlight(): void { + if (targetElement) { + targetElement.classList.remove('element-hider-target'); + targetElement = null; + } +} + +// Context menu state management +let currentContextElement: HTMLElement | null = null; +let customMenu: HTMLElement | null = null; +let contextMenuTimeout: number | null = null; +let waitingForBuiltInMenu = false; + +// Listen for right-click events to capture the target +document.addEventListener('contextmenu', (event: MouseEvent) => { + const target = event.target as HTMLElement; + + // Don't interfere with native context menus on inputs, textareas, etc. + if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) { + currentContextElement = null; + return; + } + + // Don't show menu on our own custom menu + if (target.closest(".element-hider-custom-menu")) { + return; + } + + // Close any existing custom menu + closeCustomMenu(); + + // Store the right-clicked element and context + currentContextElement = target; + highlightElement(target); + waitingForBuiltInMenu = true; + + // Store event coordinates for potential custom menu + const eventX = event.clientX; + const eventY = event.clientY; + + // Wait to see if the built-in context menu appears + contextMenuTimeout = window.setTimeout(() => { + // If we're still waiting and no built-in menu appeared, show our custom menu + if (waitingForBuiltInMenu && currentContextElement) { + event.preventDefault(); + showCustomMenu(eventX, eventY); + } + waitingForBuiltInMenu = false; + }, 150); // Wait 150ms for built-in menu + + // Don't prevent default initially - let Luna try to handle the context menu +}, true); + +// Listen for clicks to remove highlights and close custom menu +document.addEventListener('click', (event: MouseEvent) => { + const target = event.target as HTMLElement; + + // If clicking outside our custom menu, close it + if (customMenu && !target.closest(".element-hider-custom-menu")) { + closeCustomMenu(); + } + + removeHighlight(); +}, true); + +// Handle escape key to close custom menu +document.addEventListener('keydown', (event: KeyboardEvent) => { + if (event.key === "Escape" && customMenu) { + closeCustomMenu(); + removeHighlight(); + } +}); + +// Create custom context menu +function createCustomMenu(): HTMLElement { + const menu = document.createElement("div"); + menu.className = "element-hider-custom-menu"; + + // Hide Element option + const hideItem = document.createElement("button"); + hideItem.className = "element-hider-menu-item"; + hideItem.innerHTML = `Hide This Element`; + hideItem.addEventListener("click", () => { + if (currentContextElement) { + targetElement = currentContextElement; + hideTargetElement(); + closeCustomMenu(); + } + }); + + // Show All Elements option + const showAllItem = document.createElement("button"); + showAllItem.className = "element-hider-menu-item"; + showAllItem.innerHTML = `Show All Hidden Elements (${hiddenElementsArray.length})`; + showAllItem.addEventListener("click", () => { + showAllElements(); + closeCustomMenu(); + }); + + menu.appendChild(hideItem); + menu.appendChild(showAllItem); + + return menu; +} + +// Show custom context menu +function showCustomMenu(x: number, y: number): void { + closeCustomMenu(); + + customMenu = createCustomMenu(); + document.body.appendChild(customMenu); + + // Position the menu + const rect = customMenu.getBoundingClientRect(); + const finalX = Math.min(x, window.innerWidth - rect.width - 10); + const finalY = Math.min(y, window.innerHeight - rect.height - 10); + + customMenu.style.left = `${finalX}px`; + customMenu.style.top = `${finalY}px`; + + trace.msg.log(`Custom context menu opened for element: ${currentContextElement?.tagName}${currentContextElement?.className ? '.' + currentContextElement.className.split(' ').join('.') : ''}`); +} + +// Close custom context menu +function closeCustomMenu(): void { + if (customMenu) { + customMenu.remove(); + customMenu = null; + } + + if (contextMenuTimeout) { + clearTimeout(contextMenuTimeout); + contextMenuTimeout = null; + } +} + +// Try to hook into the context menu when it appears +const contextMenuObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as HTMLElement; + + // Look for Tidal's context menu + if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) { + const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement; + + if (contextMenu && currentContextElement && waitingForBuiltInMenu) { + // Built-in menu appeared, cancel custom menu timeout + waitingForBuiltInMenu = false; + if (contextMenuTimeout) { + clearTimeout(contextMenuTimeout); + contextMenuTimeout = null; + } + addElementHiderOptions(contextMenu); + } + } + } + }); + }); +}); + +// Add our options to the existing context menu +function addElementHiderOptions(contextMenu: HTMLElement): void { + // Create hide element button + const hideButton = document.createElement('button'); + hideButton.className = 'element-hider-menu-item'; + hideButton.style.cssText = ` + display: flex; + align-items: center; + padding: 8px 16px; + cursor: pointer; + color: var(--wave-color-text, #ffffff); + background: transparent; + border: none; + width: 100%; + text-align: left; + transition: background-color 0.15s ease; + font-family: inherit; + font-size: 14px; + `; + hideButton.innerHTML = `Hide This Element`; + + hideButton.addEventListener('click', () => { + if (currentContextElement) { + targetElement = currentContextElement; + hideTargetElement(); + } + }); + + // Create show all button + const showAllButton = document.createElement('button'); + showAllButton.className = 'element-hider-menu-item'; + showAllButton.style.cssText = hideButton.style.cssText; + showAllButton.innerHTML = `Show All Hidden Elements (${hiddenElementsArray.length})`; + + showAllButton.addEventListener('click', showAllElements); + + // Add hover effects + const buttons = [hideButton, showAllButton]; + + buttons.forEach(button => { + button.addEventListener('mouseenter', () => { + button.style.background = 'var(--wave-color-background-hover, #3a3a3a)'; + }); + button.addEventListener('mouseleave', () => { + button.style.background = 'transparent'; + }); + }); + + // Add a separator if the menu has other items + if (contextMenu.children.length > 0) { + const separator = document.createElement('div'); + separator.style.cssText = ` + height: 1px; + background: var(--wave-color-border, #444); + margin: 4px 8px; + `; + contextMenu.appendChild(separator); + } + + // Add our buttons + contextMenu.appendChild(hideButton); + contextMenu.appendChild(showAllButton); +} + +// Start observing for context menus +contextMenuObserver.observe(document.body, { + childList: true, + subtree: true +}); + +// Add cleanup to unloads +unloads.add(() => { + // Stop observing for context menus + contextMenuObserver.disconnect(); + + // Close any open custom menu + closeCustomMenu(); + + // Remove highlights + removeHighlight(); + + // Show all hidden elements when plugin is unloaded + showAllElements(); + + trace.msg.log("Element Hider plugin unloaded"); +}); + +trace.msg.log("Element Hider plugin loaded - Right-click any element to hide it!"); \ No newline at end of file diff --git a/plugins/element-hider-luna/src/styles.css b/plugins/element-hider-luna/src/styles.css new file mode 100644 index 0000000..4efb832 --- /dev/null +++ b/plugins/element-hider-luna/src/styles.css @@ -0,0 +1,63 @@ +/* Element Hider Styles */ + +/* Custom context menu for elements without built-in menu */ +.element-hider-custom-menu { + position: fixed; + background: var(--wave-color-background-elevated, #2a2a2a); + border: 1px solid var(--wave-color-border, #444); + border-radius: 8px; + padding: 8px 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + z-index: 999999; + min-width: 180px; + font-family: inherit; + font-size: 14px; +} + +.element-hider-menu-item { + display: flex; + align-items: center; + padding: 8px 16px; + cursor: pointer; + color: var(--wave-color-text, #ffffff); + background: transparent; + border: none; + width: 100%; + text-align: left; + transition: background-color 0.15s ease; + font-family: inherit; + font-size: 14px; +} + +.element-hider-menu-item:hover { + background: var(--wave-color-background-hover, #3a3a3a); +} + +.element-hider-menu-item:active { + background: var(--wave-color-background-active, #4a4a4a); +} + +.element-hider-menu-icon { + margin-right: 8px; + width: 16px; + height: 16px; +} + +/* Highlight the target element */ +.element-hider-target { + outline: 2px solid #ff6b6b !important; + outline-offset: 2px !important; + box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important; +} + +/* Hidden elements */ +.element-hider-hidden { + display: none !important; +} + +/* Animation for hiding */ +.element-hider-hiding { + transition: opacity 0.3s ease, transform 0.3s ease; + opacity: 0; + transform: scale(0.95); +} \ No newline at end of file