您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Provides an API for other userscripts to add tabs to a site's settings menu.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/533630/1575663/Settings%20Tab%20Manager%20%28STM%29.js
// ==UserScript== // @name Settings Tab Manager (STM) // @namespace shared-settings-manager // @version 1.1.2 // @description Provides an API for other userscripts to add tabs to a site's settings menu. // @author Gemini // @license MIT // @match https://8chan.moe/* // @match https://8chan.se/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function() { 'use strict'; const MANAGER_ID = 'SettingsTabManager'; const log = (...args) => console.log(`[${MANAGER_ID}]`, ...args); const warn = (...args) => console.warn(`[${MANAGER_ID}]`, ...args); const error = (...args) => console.error(`[${MANAGER_ID}]`, ...args); // --- Configuration --- const SELECTORS = Object.freeze({ SETTINGS_MENU: '#settingsMenu', TAB_CONTAINER: '#settingsMenu .floatingContainer > div:first-child', PANEL_CONTAINER: '#settingsMenu .menuContentPanel', SITE_TAB: '.settingsTab', SITE_PANEL: '.panelContents', SITE_SEPARATOR: '.settingsTabSeparator', }); const ACTIVE_CLASSES = Object.freeze({ TAB: 'selectedTab', PANEL: 'selectedPanel', }); const ATTRS = Object.freeze({ SCRIPT_ID: 'data-stm-script-id', MANAGED: 'data-stm-managed', }); // --- State --- let isInitialized = false; let settingsMenuEl = null; let tabContainerEl = null; let panelContainerEl = null; let activeTabId = null; const registeredTabs = new Map(); const pendingRegistrations = []; // --- Readiness Promise --- let resolveReadyPromise; const readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; }); // --- Public API Definition (MOVED EARLIER) --- // Define the API object that will be exposed and resolved by the promise. // Functions it references must be defined *before* they are called by client scripts, // but the functions themselves can be defined later in this script, thanks to hoisting. const publicApi = Object.freeze({ ready: readyPromise, registerTab: (config) => { // Implementation uses functions defined later (registerTabImpl) return registerTabImpl(config); }, activateTab: (scriptId) => { // Implementation uses functions defined later (activateTabImpl) activateTabImpl(scriptId); }, getPanelElement: (scriptId) => { // Implementation uses functions defined later (getPanelElementImpl) return getPanelElementImpl(scriptId); }, getTabElement: (scriptId) => { // Implementation uses functions defined later (getTabElementImpl) return getTabElementImpl(scriptId); } }); // --- Styling --- GM_addStyle(` /* Ensure panels added by STM behave like native ones */ ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}] { display: none; /* Hide inactive panels */ } ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}].${ACTIVE_CLASSES.PANEL} { display: block; /* Show active panel */ } /* Optional: Basic styling for the added tabs */ ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.MANAGED}] { cursor: pointer; } `); // --- Core Logic Implementation Functions --- // (Functions like findSettingsElements, deactivateCurrentTab, activateTab, handleTabClick, etc.) /** Finds the essential DOM elements for the settings UI. Returns true if all found. */ function findSettingsElements() { settingsMenuEl = document.querySelector(SELECTORS.SETTINGS_MENU); if (!settingsMenuEl) return false; tabContainerEl = settingsMenuEl.querySelector(SELECTORS.TAB_CONTAINER); panelContainerEl = settingsMenuEl.querySelector(SELECTORS.PANEL_CONTAINER); if (!tabContainerEl) { warn('Tab container not found within settings menu using selector:', SELECTORS.TAB_CONTAINER); return false; } if (!panelContainerEl) { warn('Panel container not found within settings menu using selector:', SELECTORS.PANEL_CONTAINER); return false; } return true; } /** Deactivates the currently active STM tab (if any). */ function deactivateCurrentTab() { if (activeTabId && registeredTabs.has(activeTabId)) { const config = registeredTabs.get(activeTabId); const tab = tabContainerEl?.querySelector(`span[${ATTRS.SCRIPT_ID}="${activeTabId}"]`); const panel = panelContainerEl?.querySelector(`div[${ATTRS.SCRIPT_ID}="${activeTabId}"]`); tab?.classList.remove(ACTIVE_CLASSES.TAB); panel?.classList.remove(ACTIVE_CLASSES.PANEL); if (panel) panel.style.display = 'none'; try { config.onDeactivate?.(panel, tab); } catch (e) { error(`Error during onDeactivate for ${activeTabId}:`, e); } activeTabId = null; } // Also remove active class from any site tabs/panels managed outside STM panelContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_PANEL}.${ACTIVE_CLASSES.PANEL}:not([${ATTRS.MANAGED}])`) .forEach(p => p.classList.remove(ACTIVE_CLASSES.PANEL)); tabContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_TAB}.${ACTIVE_CLASSES.TAB}:not([${ATTRS.MANAGED}])`) .forEach(t => t.classList.remove(ACTIVE_CLASSES.TAB)); } /** Internal implementation for activating a tab */ function activateTab(scriptId) { // Renamed from activateTabImpl for clarity within scope if (!registeredTabs.has(scriptId) || !tabContainerEl || !panelContainerEl) { warn(`Cannot activate tab: ${scriptId}. Not registered or containers not found.`); return; } if (activeTabId === scriptId) return; // Already active deactivateCurrentTab(); // Deactivate previous one first const config = registeredTabs.get(scriptId); const tab = tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${scriptId}"]`); const panel = panelContainerEl.querySelector(`div[${ATTRS.SCRIPT_ID}="${scriptId}"]`); if (!tab || !panel) { error(`Tab or Panel element not found for ${scriptId} during activation.`); return; } tab.classList.add(ACTIVE_CLASSES.TAB); panel.classList.add(ACTIVE_CLASSES.PANEL); panel.style.display = 'block'; // Ensure it's visible activeTabId = scriptId; try { config.onActivate?.(panel, tab); } catch (e) { error(`Error during onActivate for ${scriptId}:`, e); } } /** Handles clicks within the tab container. */ function handleTabClick(event) { const clickedTab = event.target.closest(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`); if (clickedTab) { const scriptId = clickedTab.getAttribute(ATTRS.SCRIPT_ID); if (scriptId) { activateTab(scriptId); // Call the internal activate function } } else { if (event.target.closest(SELECTORS.SITE_TAB) && !event.target.closest(`span[${ATTRS.MANAGED}]`)) { deactivateCurrentTab(); } } } /** Attaches the main click listener to the tab container. */ function attachTabClickListener() { if (!tabContainerEl) return; tabContainerEl.removeEventListener('click', handleTabClick, true); tabContainerEl.addEventListener('click', handleTabClick, true); log('Tab click listener attached.'); } /** Helper to create a separator span */ function createSeparator(scriptId) { const separator = document.createElement('span'); separator.className = SELECTORS.SITE_SEPARATOR ? SELECTORS.SITE_SEPARATOR.substring(1) : 'settings-tab-separator-fallback'; separator.setAttribute(ATTRS.MANAGED, 'true'); separator.setAttribute('data-stm-separator-for', scriptId); separator.textContent = '|'; separator.style.cursor = 'default'; return separator; } /** Creates and inserts the tab and panel elements for a given script config. */ function createTabAndPanel(config) { if (!tabContainerEl || !panelContainerEl) { error(`Cannot create tab/panel for ${config.scriptId}: Containers not found.`); return; } if (tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${config.scriptId}"]`)) { log(`Tab already exists for ${config.scriptId}, skipping creation.`); return; } log(`Creating tab/panel for: ${config.scriptId}`); // --- Create Tab --- const newTab = document.createElement('span'); newTab.className = SELECTORS.SITE_TAB.substring(1); newTab.textContent = config.tabTitle; newTab.setAttribute(ATTRS.SCRIPT_ID, config.scriptId); newTab.setAttribute(ATTRS.MANAGED, 'true'); newTab.setAttribute('title', `${config.tabTitle} (Settings by ${config.scriptId})`); const desiredOrder = typeof config.order === 'number' ? config.order : Infinity; newTab.setAttribute('data-stm-order', desiredOrder); // Store order attribute // --- Create Panel --- const newPanel = document.createElement('div'); newPanel.className = SELECTORS.SITE_PANEL.substring(1); newPanel.setAttribute(ATTRS.SCRIPT_ID, config.scriptId); newPanel.setAttribute(ATTRS.MANAGED, 'true'); newPanel.id = `${MANAGER_ID}-${config.scriptId}-panel`; // --- Insertion Logic (with basic ordering) --- let inserted = false; const existingStmTabs = Array.from(tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`)); // Get combined list of STM tabs and separators for easier sorting/insertion const elementsToSort = existingStmTabs.map(tab => ({ element: tab, order: parseInt(tab.getAttribute('data-stm-order') || Infinity, 10), isSeparator: false, separatorFor: tab.getAttribute(ATTRS.SCRIPT_ID) })); // Find separators associated with STM tabs tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}] + ${SELECTORS.SITE_SEPARATOR}[${ATTRS.MANAGED}], ${SELECTORS.SITE_SEPARATOR}[${ATTRS.MANAGED}] + span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`).forEach(sep => { let associatedTabId = sep.getAttribute('data-stm-separator-for'); if (associatedTabId) { let associatedTab = elementsToSort.find(item => item.separatorFor === associatedTabId); if (associatedTab) { elementsToSort.push({ element: sep, order: associatedTab.order, isSeparator: true, separatorFor: associatedTabId }); } } }); // Simplified: Find the correct place based on order among existing STM tabs let insertBeforeElement = null; for (const existingTab of existingStmTabs.sort((a, b) => parseInt(a.getAttribute('data-stm-order') || Infinity, 10) - parseInt(b.getAttribute('data-stm-order') || Infinity, 10))) { const existingOrder = parseInt(existingTab.getAttribute('data-stm-order') || Infinity, 10); if (desiredOrder < existingOrder) { insertBeforeElement = existingTab; break; } } const newSeparator = createSeparator(config.scriptId); // Create separator regardless if (insertBeforeElement) { // Check if the element before the target is a separator. If so, insert before that separator. const prevElement = insertBeforeElement.previousElementSibling; if (prevElement && prevElement.matches(`${SELECTORS.SITE_SEPARATOR}[${ATTRS.MANAGED}]`)) { tabContainerEl.insertBefore(newSeparator, prevElement); tabContainerEl.insertBefore(newTab, prevElement); // Insert tab *after* its separator } else { // Insert separator and then tab right before the target element tabContainerEl.insertBefore(newSeparator, insertBeforeElement); tabContainerEl.insertBefore(newTab, insertBeforeElement); } inserted = true; } if (!inserted) { // Append at the end of other STM tabs (potentially before site's last tabs) const lastStmTab = existingStmTabs.pop(); // Get the last one from the originally selected list if (lastStmTab) { // Insert after the last STM tab tabContainerEl.insertBefore(newSeparator, lastStmTab.nextSibling); // Insert separator first tabContainerEl.insertBefore(newTab, newSeparator.nextSibling); // Insert tab after separator } else { // This is the first STM tab being added, append separator and tab tabContainerEl.appendChild(newSeparator); tabContainerEl.appendChild(newTab); } } // Append Panel panelContainerEl.appendChild(newPanel); // --- Initialize Panel Content --- try { Promise.resolve(config.onInit(newPanel, newTab)).catch(e => { error(`Error during async onInit for ${config.scriptId}:`, e); newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`; }); } catch (e) { error(`Error during sync onInit for ${config.scriptId}:`, e); newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`; } } /** Process all pending registrations. */ function processPendingRegistrations() { if (!isInitialized) return; log(`Processing ${pendingRegistrations.length} pending registrations...`); while (pendingRegistrations.length > 0) { const config = pendingRegistrations.shift(); if (!registeredTabs.has(config.scriptId)) { registeredTabs.set(config.scriptId, config); createTabAndPanel(config); } else { warn(`Script ID ${config.scriptId} was already registered. Skipping pending registration.`); } } } // --- Initialization and Observation --- /** Main initialization routine. */ function initializeManager() { if (!findSettingsElements()) { log('Settings elements not found on init check.'); return false; } if (isInitialized) { log('Manager already initialized.'); attachTabClickListener(); // Re-attach listener just in case return true; } log('Initializing Settings Tab Manager...'); attachTabClickListener(); isInitialized = true; log('Manager is ready.'); // NOW it's safe to resolve the promise with publicApi resolveReadyPromise(publicApi); processPendingRegistrations(); return true; } // Observer const observer = new MutationObserver((mutationsList, obs) => { let foundMenu = !!settingsMenuEl; let potentialReInit = false; if (!foundMenu) { for (const mutation of mutationsList) { /* ... same logic as before ... */ if (mutation.addedNodes) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const menu = (node.matches && node.matches(SELECTORS.SETTINGS_MENU)) ? node : (node.querySelector ? node.querySelector(SELECTORS.SETTINGS_MENU) : null); if (menu) { foundMenu = true; potentialReInit = true; log('Settings menu detected in DOM.'); break; } } } } if (foundMenu) break; } } if (foundMenu) { if (!isInitialized || !settingsMenuEl || !tabContainerEl || !panelContainerEl || potentialReInit) { if (initializeManager()) { // Optional: obs.disconnect(); } else { log('Initialization check failed, observer remains active.'); } } } }); observer.observe(document.body, { childList: true, subtree: true }); log('Mutation observer started for settings menu detection.'); initializeManager(); // Attempt initial // --- API Implementation Functions (linked from publicApi object) --- function registerTabImpl(config) { // Renamed from registerTab if (!config || typeof config !== 'object') { /* ... validation ... */ error('Registration failed: Invalid config object provided.'); return false; } const { scriptId, tabTitle, onInit } = config; if (typeof scriptId !== 'string' || !scriptId.trim()) { /* ... validation ... */ error('Registration failed: Invalid or missing scriptId.', config); return false; } if (typeof tabTitle !== 'string' || !tabTitle.trim()) { /* ... validation ... */ error('Registration failed: Invalid or missing tabTitle.', config); return false; } if (typeof onInit !== 'function') { /* ... validation ... */ error('Registration failed: onInit must be a function.', config); return false; } if (registeredTabs.has(scriptId)) { /* ... validation ... */ warn(`Registration failed: Script ID "${scriptId}" is already registered.`); return false; } if (config.onActivate && typeof config.onActivate !== 'function') { /* ... validation ... */ error(`Registration for ${scriptId} failed: onActivate must be a function.`); return false; } if (config.onDeactivate && typeof config.onDeactivate !== 'function') { /* ... validation ... */ error(`Registration for ${scriptId} failed: onDeactivate must be a function.`); return false; } log(`Registration accepted for: ${scriptId}`); const registrationData = { ...config }; if (isInitialized) { registeredTabs.set(scriptId, registrationData); createTabAndPanel(registrationData); } else { log(`Manager not ready, queueing registration for ${scriptId}`); pendingRegistrations.push(registrationData); } return true; } function activateTabImpl(scriptId) { // Renamed from activateTab if (isInitialized) { activateTab(scriptId); // Calls the internal function } else { warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`); } } function getPanelElementImpl(scriptId) { // Renamed from getPanelElement if (!isInitialized || !panelContainerEl) return null; return panelContainerEl.querySelector(`div[${ATTRS.SCRIPT_ID}="${scriptId}"]`); } function getTabElementImpl(scriptId) { // Renamed from getTabElement if (!isInitialized || !tabContainerEl) return null; return tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${scriptId}"]`); } // --- Global Exposure --- if (window.SettingsTabManager && window.SettingsTabManager !== publicApi) { warn('window.SettingsTabManager is already defined by another script or instance!'); } else if (!window.SettingsTabManager) { Object.defineProperty(window, 'SettingsTabManager', { value: publicApi, // Expose the predefined API object writable: false, configurable: true }); log('SettingsTabManager API exposed on window.'); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址