您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Provides an API for other userscripts to add tabs to a site's settings menu, with a single separator and improved interaction logic.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/533630/1575936/Settings%20Tab%20Manager%20%28STM%29.js
// ==UserScript== // @name Settings Tab Manager (STM) // @namespace shared-settings-manager // @version 1.1.3 // @description Provides an API for other userscripts to add tabs to a site's settings menu, with a single separator and improved interaction logic. // @author Gemini & User Input // @license MIT // @match https://8chan.moe/* // @match https://8chan.se/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- Keep Constants, State (isSeparatorAdded etc.), Promise, publicApi, Styling the same --- 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); const SELECTORS = Object.freeze({ /* ... same ... */ 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({ /* ... same ... */ TAB: 'selectedTab', PANEL: 'selectedPanel', }); const ATTRS = Object.freeze({ /* ... same ... */ SCRIPT_ID: 'data-stm-script-id', MANAGED: 'data-stm-managed', SEPARATOR: 'data-stm-main-separator', ORDER: 'data-stm-order', }); let isInitialized = false; let settingsMenuEl = null; let tabContainerEl = null; let panelContainerEl = null; let activeTabId = null; // Holds the scriptId of the currently active STM tab, null otherwise const registeredTabs = new Map(); const pendingRegistrations = []; let isSeparatorAdded = false; let resolveReadyPromise; const readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; }); const publicApi = Object.freeze({ /* ... same ... */ ready: readyPromise, registerTab: (config) => registerTabImpl(config), activateTab: (scriptId) => activateTabImpl(scriptId), getPanelElement: (scriptId) => getPanelElementImpl(scriptId), getTabElement: (scriptId) => getTabElementImpl(scriptId) }); GM_addStyle(`/* ... same styles ... */ ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}] { display: none; } ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}].${ACTIVE_CLASSES.PANEL} { display: block; } ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}] { cursor: pointer; } ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.SEPARATOR}] { cursor: default; margin: 0 5px; } `); // --- Core Logic Implementation Functions --- function findSettingsElements() { /* ... same ... */ settingsMenuEl = document.querySelector(SELECTORS.SETTINGS_MENU); if (!settingsMenuEl) return false; tabContainerEl = settingsMenuEl.querySelector(SELECTORS.TAB_CONTAINER); panelContainerEl = settingsMenuEl.querySelector(SELECTORS.PANEL_CONTAINER); if (!tabContainerEl || !panelContainerEl) { /* ... warning ... */ return false; } if (!document.body.contains(settingsMenuEl) || !document.body.contains(tabContainerEl) || !document.body.contains(panelContainerEl)) { /* ... warning ... */ return false; } return true; } /** * Deactivates the currently active STM tab (specified by activeTabId). * Does NOT interfere with native site tab classes. */ function deactivateCurrentStmTab() { // Renamed for clarity if (!activeTabId) return; // No STM tab active const config = registeredTabs.get(activeTabId); if (!config) { warn(`Config not found for supposedly active tab ID: ${activeTabId}`); activeTabId = null; // Clear potentially invalid ID return; } const tab = getTabElementImpl(activeTabId); const panel = getPanelElementImpl(activeTabId); if (tab) { tab.classList.remove(ACTIVE_CLASSES.TAB); // log(`Deactivated tab class for: ${activeTabId}`); } else { warn(`Could not find tab element for ${activeTabId} during deactivation.`); } if (panel) { panel.classList.remove(ACTIVE_CLASSES.PANEL); panel.style.display = 'none'; // Explicitly hide // log(`Deactivated panel class/display for: ${activeTabId}`); } else { warn(`Could not find panel element for ${activeTabId} during deactivation.`); } // Call the script's deactivate hook try { config.onDeactivate?.(panel, tab); // Pass potentially null elements if lookup failed } catch (e) { error(`Error during onDeactivate for ${activeTabId}:`, e); } activeTabId = null; // Clear the active STM tab ID *after* processing } /** * Activates a specific STM tab. Handles deactivation of any previously active STM tab. */ function activateStmTab(scriptId) { // Renamed for clarity if (!registeredTabs.has(scriptId) || !tabContainerEl || !panelContainerEl) { error(`Cannot activate tab: ${scriptId}. Not registered or containers not found.`); // Changed from warn to error return; } if (activeTabId === scriptId) { // log(`Tab ${scriptId} is already active.`); return; // Already active } // --- Deactivation Phase --- // Deactivate the *currently active STM tab* first. deactivateCurrentStmTab(); // This now ONLY touches the STM tab defined by previous activeTabId // --- Activation Phase --- const config = registeredTabs.get(scriptId); const tab = getTabElementImpl(scriptId); const panel = getPanelElementImpl(scriptId); if (!tab || !panel) { error(`Tab or Panel element not found for ${scriptId} during activation.`); // Attempt to clean up partly activated state? Maybe not needed. return; // Cannot proceed } // **Crucially, ensure native tabs are visually deselected.** // We rely on the site's own handler for native tabs, but if switching // from native to STM, we need to ensure the native one is visually cleared. 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)); // Activate the new STM tab/panel tab.classList.add(ACTIVE_CLASSES.TAB); panel.classList.add(ACTIVE_CLASSES.PANEL); panel.style.display = 'block'; // Ensure visible const previouslyActiveId = activeTabId; // Store before overwriting activeTabId = scriptId; // Set the new active STM tab ID *before* calling onActivate // log(`Activated tab/panel for: ${scriptId}`); // Call the script's activation hook try { config.onActivate?.(panel, tab); } catch (e) { error(`Error during onActivate for ${scriptId}:`, e); // Should we revert activation? Tricky. Logged error is usually sufficient. } } /** Handles clicks within the tab container to switch tabs. */ function handleTabClick(event) { // Check if an STM-managed tab was clicked const clickedStmTab = event.target.closest(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`); if (clickedStmTab) { event.stopPropagation(); // Stop event from bubbling further (like to site handlers) const scriptId = clickedStmTab.getAttribute(ATTRS.SCRIPT_ID); if (scriptId && scriptId !== activeTabId) { // log(`STM tab clicked: ${scriptId}`); activateStmTab(scriptId); // Activate the clicked STM tab } else if (scriptId && scriptId === activeTabId){ // log(`Clicked already active STM tab: ${scriptId}`); // Do nothing if clicking the already active tab } return; // Handled by STM } // Check if a native site tab was clicked (and NOT an STM tab) const clickedSiteTab = event.target.closest(`${SELECTORS.SITE_TAB}:not([${ATTRS.MANAGED}])`); if (clickedSiteTab) { // log(`Native site tab clicked.`); // If an STM tab was active, deactivate it visually and internally. // Let the site's own handler manage the activation of the native tab. if (activeTabId) { // log(`Deactivating current STM tab (${activeTabId}) due to native tab click.`); deactivateCurrentStmTab(); } // **Do not** stop propagation here. Let the site's own click handler run. return; } // Check if the separator was clicked (do nothing) if (event.target.closest(`span[${ATTRS.SEPARATOR}]`)) { event.stopPropagation(); // log("Separator clicked."); return; } // If click was somewhere else within the container but not on a tab, do nothing special. // log("Clicked non-tab area within container."); } function attachTabClickListener() { /* ... same ... */ if (!tabContainerEl) return; tabContainerEl.removeEventListener('click', handleTabClick, true); tabContainerEl.addEventListener('click', handleTabClick, true); // Keep capture=true log('Tab click listener attached.'); } function createSeparator() { /* ... same ... */ 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(ATTRS.SEPARATOR, 'true'); separator.textContent = '|'; return separator; } function createTabAndPanel(config) { /* ... mostly same ... */ // ... checks ... // ... create Tab element (newTab) ... // ... create Panel element (newPanel) ... // ... Insertion Logic (Single Separator & Ordered Tabs) ... // Find insertBeforeTab based on order... // Check isFirstStmTabBeingAdded... // Add separatorInstance if needed... // Insert separatorInstance and newTab... // Append newPanel... if (!tabContainerEl || !panelContainerEl) { /* ... error ... */ return; } if (tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${config.scriptId}"]`)) { /* ... log skip ... */ return; } log(`Creating tab/panel for: ${config.scriptId}`); const newTab = document.createElement('span'); /* ... set attributes ... */ 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(ATTRS.ORDER, desiredOrder); const newPanel = document.createElement('div'); /* ... set attributes ... */ 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`; let insertBeforeTab = null; const existingStmTabs = Array.from(tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`)); existingStmTabs.sort((a, b) => (parseInt(a.getAttribute(ATTRS.ORDER) || Infinity, 10)) - (parseInt(b.getAttribute(ATTRS.ORDER) || Infinity, 10))); for (const existingTab of existingStmTabs) { if (desiredOrder < (parseInt(existingTab.getAttribute(ATTRS.ORDER) || Infinity, 10))) { insertBeforeTab = existingTab; break; } } const isFirstStmTabBeingAdded = existingStmTabs.length === 0; let separatorInstance = null; if (!isSeparatorAdded && isFirstStmTabBeingAdded) { separatorInstance = createSeparator(); isSeparatorAdded = true; log('Adding the main STM separator.'); } if (insertBeforeTab) { if (separatorInstance) tabContainerEl.insertBefore(separatorInstance, insertBeforeTab); tabContainerEl.insertBefore(newTab, insertBeforeTab); } else { if (separatorInstance) tabContainerEl.appendChild(separatorInstance); tabContainerEl.appendChild(newTab); } panelContainerEl.appendChild(newPanel); // --- Initialize Panel Content --- (Keep Promise.resolve wrapper) 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>`; } } function processPendingRegistrations() { /* ... same, ensure sorting ... */ if (!isInitialized) return; log(`Processing ${pendingRegistrations.length} pending registrations...`); pendingRegistrations.sort((a, b) => { const orderA = typeof a.order === 'number' ? a.order : Infinity; const orderB = typeof b.order === 'number' ? b.order : Infinity; return orderA - orderB; }); 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.`); } } log('Finished processing pending registrations.'); } // --- Initialization and Observation --- function initializeManager() { /* ... same, calls findSettingsElements, attachTabClickListener, processPendingRegistrations ... */ if (!findSettingsElements()) { return false; } if (isInitialized && settingsMenuEl && tabContainerEl && panelContainerEl) { attachTabClickListener(); return true; } log('Initializing Settings Tab Manager...'); attachTabClickListener(); isInitialized = true; log('Manager is ready.'); resolveReadyPromise(publicApi); processPendingRegistrations(); return true; } const observer = new MutationObserver(/* ... same observer logic ... */ (mutationsList, obs) => { let needsReInitCheck = false; if (!isInitialized && document.querySelector(SELECTORS.SETTINGS_MENU)) { needsReInitCheck = true; } else if (isInitialized && settingsMenuEl && !document.body.contains(settingsMenuEl)) { warn('Settings menu seems to have been removed from DOM.'); isInitialized = false; settingsMenuEl = null; tabContainerEl = null; panelContainerEl = null; isSeparatorAdded = false; activeTabId = null; needsReInitCheck = true; } if (!settingsMenuEl || needsReInitCheck) { for (const mutation of mutationsList) { /* ... find menu ... */ 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) { log('Settings menu detected...'); needsReInitCheck = true; break; } } } } if (needsReInitCheck) break; } } if (needsReInitCheck) { setTimeout(() => { if (initializeManager()) { log('Manager initialized/re-initialized successfully via MutationObserver.'); } }, 0); } }); observer.observe(document.body, { childList: true, subtree: true }); log('Mutation observer started...'); setTimeout(initializeManager, 0); // Initial attempt // --- API Implementation Functions --- function registerTabImpl(config) { /* ... same validation, sorting pending queue ... */ if (!config || typeof config !== 'object') { error('Registration failed: Invalid config object.'); return false; } const { scriptId, tabTitle, onInit } = config; /* ... validate scriptId, tabTitle, onInit, optionals ... */ if (typeof scriptId !== 'string' || !scriptId.trim()) { error('Reg failed: Invalid scriptId.'); return false; } if (typeof tabTitle !== 'string' || !tabTitle.trim()) { error('Reg failed: Invalid tabTitle.'); return false; } if (typeof onInit !== 'function') { error('Reg failed: onInit not function.'); return false; } if (registeredTabs.has(scriptId) || pendingRegistrations.some(p => p.scriptId === scriptId)) { warn(`Reg failed: ID "${scriptId}" already registered/pending.`); return false; } // ... more optional validation ... 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); pendingRegistrations.sort((a, b) => { const orderA = typeof a.order === 'number' ? a.order : Infinity; const orderB = typeof b.order === 'number' ? b.order : Infinity; return orderA - orderB; }); } return true; } // Public API function now calls the renamed internal function function activateTabImpl(scriptId) { if (typeof scriptId !== 'string' || !scriptId.trim()) { error('activateTab failed: Invalid scriptId.'); return; } if (isInitialized) { activateStmTab(scriptId); // Call the internal function } else { warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`); } } function getPanelElementImpl(scriptId) { /* ... same ... */ if (!isInitialized || !panelContainerEl) return null; if (typeof scriptId !== 'string' || !scriptId.trim()) return null; return panelContainerEl.querySelector(`div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`); } function getTabElementImpl(scriptId) { /* ... same ... */ if (!isInitialized || !tabContainerEl) return null; if (typeof scriptId !== 'string' || !scriptId.trim()) return null; return tabContainerEl.querySelector(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`); } // --- Global Exposure --- if (window.SettingsTabManager && window.SettingsTabManager !== publicApi) { /* ... warning ... */ } else if (!window.SettingsTabManager) { /* ... define property ... */ Object.defineProperty(window, 'SettingsTabManager', { value: publicApi, writable: false, configurable: true }); log('SettingsTabManager API exposed on window.'); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址