您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Clicks specified buttons across tabs using the Broadcast Channel API and closes tabs after successful submission.
// ==UserScript== // @name Click buttons across tabs // @namespace https://musicbrainz.org/user/chaban // @version 2.6 // @tag ai-created // @description Clicks specified buttons across tabs using the Broadcast Channel API and closes tabs after successful submission. // @author chaban // @license MIT // @match *://*.musicbrainz.org/* // @match *://magicisrc.kepstin.ca/* // @match *://magicisrc-beta.kepstin.ca/* // @run-at document-start // @grant GM.info // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM.getValue // @grant GM.setValue // @grant window.close // ==/UserScript== (async function () { 'use strict'; const scriptName = GM.info.script.name; console.log(`%c[${scriptName}] Script initialization started on ${location.href}`, 'font-weight: bold;'); /** * @typedef {Object} SiteConfig * @property {string|string[]} hostnames - A single hostname string or an array of hostname strings. * @property {string|string[]} paths - A single path string or an array of path strings (must match ending of pathname). * @property {string} buttonSelector - The CSS selector string for the button to be clicked. * @property {string} [channelName] - The name of the BroadcastChannel to use. Omit if autoClick is true. * @property {string} [messageTrigger] - The message data that triggers the button click. Omit if autoClick is true. * @property {string} [menuCommandName] - The name to display in the Tampermonkey/Greasemonkey menu. Omit if autoClick is true. * @property {(RegExp|string)[]} [successUrlPatterns] - An array of RegExp or string patterns to match against the URL to indicate a successful submission. * @property {boolean} [shouldCloseAfterSuccess=false] - Whether to attempt closing the tab after a successful submission. Note: Browser security heavily restricts `window.close()`. * @property {boolean} [autoClick=false] - If true, the button will be clicked automatically when found, without needing a broadcast message. * @property {boolean} [disableDelay=false] - If true, this button click will happen immediately (0 delay). */ /** * Configuration for different websites and their button click settings. * Order matters for `autoClick` rules and how `configForSuccessCheck` is found. * @type {SiteConfig[]} */ const siteConfigurations = [ { hostnames: ['musicbrainz.org'], paths: ['/edit-relationships'], buttonSelector: '.rel-editor > button', autoClick: true, disableDelay: true, successUrlPatterns: [], shouldCloseAfterSuccess: false }, { hostnames: ['musicbrainz.org'], paths: [ '/edit', '/edit-relationships', '/add-cover-art' ], channelName: 'mb_edit_channel', messageTrigger: 'submit-edit', buttonSelector: 'button.submit.positive[type="submit"]', menuCommandName: 'MusicBrainz: Submit Edit (All Tabs)', successUrlPatterns: [ /^https?:\/\/(?:beta\.)?musicbrainz\.org\/(?!collection\/)[^/]+\/[a-f0-9\-]{36}(?:\/cover-art)?\/?$/, ], shouldCloseAfterSuccess: true }, { hostnames: ['magicisrc.kepstin.ca', 'magicisrc-beta.kepstin.ca'], paths: ['/'], channelName: 'magicisrc_submit_channel', messageTrigger: 'submit-isrcs', buttonSelector: '[onclick^="doSubmitISRCs"]', menuCommandName: 'MagicISRC: Submit ISRCs (All Tabs)', successUrlPatterns: [ /\?.*submit=1/ ], shouldCloseAfterSuccess: true } ]; const SUBMISSION_TRIGGERED_FLAG = 'broadcastChannelSubmissionState'; const GLOBAL_CLOSE_TAB_CHANNEL_NAME = 'global_close_tab_channel'; const GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER = 'close-this-tab'; const GLOBAL_CLOSE_TAB_MENU_COMMAND_NAME = 'Global: Close This Tab (All Tabs)'; const MIN_CLICK_DELAY_SECONDS = 0; const DELAY_SCALING_FACTOR = 120; const DELAY_MODE_SETTING = 'mb_button_clicker_delayMode'; const STATIC_MAX_DELAY_SETTING = 'mb_button_clicker_staticMaxDelay'; const DISABLE_AUTO_CLOSE_SETTING = 'mb_button_clicker_disableAutoClose'; const MAGICISRC_ENABLE_AUTO_RELOAD = 'magicisrc_enableAutoReload'; let registeredMenuCommandIDs = []; /** * Sends a message to the specified BroadcastChannel. * @param {string} channelName * @param {string} message */ function sendMessageToChannel(channelName, message) { try { new BroadcastChannel(channelName).postMessage(message); console.log(`[${scriptName}] Sent message "${message}" to channel "${channelName}".`); } catch (error) { console.error(`[${scriptName}] Error sending message to channel "${channelName}":`, error); } } /** * Checks if the current page indicates a successful submission based on the given config's URL patterns. * @param {SiteConfig} config - The site configuration. * @param {boolean} [quiet=false] - If true, suppresses console logs for matches/no matches. * @returns {boolean} */ function isSubmissionSuccessful(config, quiet = false) { if (!config.successUrlPatterns || config.successUrlPatterns.length === 0) { return false; } for (const pattern of config.successUrlPatterns) { const matchResult = (typeof pattern === 'string') ? location.href.includes(pattern) : pattern.test(location.href); if (matchResult) { if (!quiet) { console.log(`[${scriptName}] URL "${location.href}" matches success pattern "${pattern}".`); } return true; } } if (!quiet) { console.log(`[${scriptName}] URL "${location.href}" does not match any success pattern.`); } return false; } /** * Attempts to close the current tab. * @param {number} delayMs - Optional delay in milliseconds before attempting to close. * @returns {void} */ function attemptCloseTab(delayMs = 200) { console.log(`[${scriptName}] Attempting to close tab in ${delayMs}ms.`); setTimeout(() => { try { window.close(); console.log(`[${scriptName}] Successfully called window.close() after delay.`); } catch (e) { console.warn(`[${scriptName}] Failed to close tab automatically via window.close(). This is expected due to browser security restrictions.`, e); } }, delayMs); } /** * Checks the current page against the success criteria for the relevant site, * and closes the tab if successful, unless auto-close is disabled. * @param {SiteConfig} currentConfigForSuccessCheck - The site config relevant to the current hostname. * @returns {Promise<void>} */ async function checkAndCloseIfSuccessful(currentConfigForSuccessCheck) { const storedSubmissionState = sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG); if (!storedSubmissionState) { console.log(`[${scriptName}] No stored submission state. Skipping close check.`); return; } const triggered = storedSubmissionState === 'true'; const disableAutoClose = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false); const isCurrentlySuccessful = isSubmissionSuccessful(currentConfigForSuccessCheck); if (triggered && currentConfigForSuccessCheck.shouldCloseAfterSuccess) { console.log(`[${scriptName}] Checking for submission success on "${location.href}".`); if (isCurrentlySuccessful) { console.log(`[${scriptName}] Submission successful. Clearing SUBMISSION_TRIGGERED_FLAG.`); sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG); if (disableAutoClose) { console.info(`%c[${scriptName}] Submission successful, but automatic tab closing is DISABLED by user setting.`, 'color: orange; font-weight: bold;'); } else { console.log(`%c[${scriptName}] Submission successful. Closing tab.`, 'color: green; font-weight: bold;'); attemptCloseTab(); } } else { console.info(`%c[${scriptName}] Submission was triggered, but URL does not match success criteria.`, 'color: orange;'); } } else { console.log(`[${scriptName}] Not checking for success: triggered is false, or closing is not configured for this site.`); } } /** * Retrieves the current delay mode from storage. * @returns {Promise<string>} 'dynamic' or 'static'. Defaults to 'dynamic'. */ async function getDelayMode() { return await GM.getValue(DELAY_MODE_SETTING, 'dynamic'); } /** * Determines and returns the current minimum and maximum click delays based on settings. * @returns {Promise<{min: number, max: number}>} */ async function getCurrentClickDelayRange() { const delayMode = await getDelayMode(); let currentMinDelay; let currentMaxDelay; let delayModeLog; if (delayMode === 'static') { currentMinDelay = 0; currentMaxDelay = await GM.getValue(STATIC_MAX_DELAY_SETTING, 6); delayModeLog = `Delays set to STATIC (random 0 to ${currentMaxDelay}s).`; } else { const logicalCores = Math.max(1, navigator.hardwareConcurrency || 1); currentMinDelay = MIN_CLICK_DELAY_SECONDS; currentMaxDelay = Math.max(currentMinDelay, Math.round(DELAY_SCALING_FACTOR / logicalCores)); delayModeLog = `Delays set to DYNAMIC (based on ${logicalCores} cores): ${currentMinDelay} to ${currentMaxDelay} seconds.`; } console.log(`[${scriptName}] ${delayModeLog}`); return { min: currentMinDelay, max: currentMaxDelay }; } /** * Calculates a random delay based on the current delay mode settings. * @param {boolean} [disableDelayOverride=false] - If true, forces a 0ms delay. * @returns {Promise<number>} Delay in milliseconds. */ async function getCalculatedDelay(disableDelayOverride = false) { if (disableDelayOverride) { console.log(`[${scriptName}] Delay disabled for this action as per config.`); return 0; } const { min: currentMinDelay, max: currentMaxDelay } = await getCurrentClickDelayRange(); const delaySeconds = Math.floor(Math.random() * (currentMaxDelay - currentMinDelay + 1)) + currentMinDelay; return delaySeconds * 1000; } /** * Sets up menu commands for the userscript. * This function is responsible for clearing and re-registering ALL commands * to ensure dynamic display names and conditional commands work correctly. */ async function setupMenuCommands() { registeredMenuCommandIDs.forEach(id => { try { GM_unregisterMenuCommand(id); } catch (e) { } }); registeredMenuCommandIDs = []; const registerCommand = (name, func) => { const id = GM_registerMenuCommand(name, func); registeredMenuCommandIDs.push(id); }; const delayMode = await getDelayMode(); const disableAutoClose = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false); const enableMagicISRCReload = await GM.getValue(MAGICISRC_ENABLE_AUTO_RELOAD, true); if (delayMode === 'dynamic') { registerCommand('Delay Mode: DYNAMIC', async () => { await GM.setValue(DELAY_MODE_SETTING, 'static'); console.log(`[${scriptName}] Delay mode switched to: STATIC.`); await getCurrentClickDelayRange(); await setupMenuCommands(); }); } else { registerCommand('Delay Mode: STATIC', async () => { await GM.setValue(DELAY_MODE_SETTING, 'dynamic'); console.log(`[${scriptName}] Delay mode switched to: DYNAMIC.`); await getCurrentClickDelayRange(); await setupMenuCommands(); }); const currentStaticMaxValue = await GM.getValue(STATIC_MAX_DELAY_SETTING, 6); registerCommand(`Set Static Max Delay (Current: ${currentStaticMaxValue}s)`, async () => { let newValue = prompt(`Enter maximum STATIC delay in seconds (Current: ${currentStaticMaxValue}). A random delay between 0 and this value (inclusive) will be used.`); if (newValue === null) return; newValue = parseFloat(newValue); if (isNaN(newValue) || newValue < 0) { console.warn(`[${scriptName}] Invalid input for static max delay: "${newValue}". Please enter a non-negative number.`); alert("Invalid input. Please enter a non-negative number."); return; } const actualNewValue = Math.floor(newValue); await GM.setValue(STATIC_MAX_DELAY_SETTING, actualNewValue); console.log(`[${scriptName}] Static maximum delay set to ${actualNewValue} seconds.`); await getCurrentClickDelayRange(); await setupMenuCommands(); }); } registerCommand(`Auto Close Tabs: ${disableAutoClose ? 'DISABLED' : 'ENABLED'}`, async () => { const currentState = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false); const newState = !currentState; await GM.setValue(DISABLE_AUTO_CLOSE_SETTING, newState); console.log(`[${scriptName}] Automatic tab closing is now ${newState ? 'DISABLED' : 'ENABLED'}.`); await setupMenuCommands(); }); registerCommand(`MagicISRC Auto Reload: ${enableMagicISRCReload ? 'ENABLED' : 'DISABLED'}`, async () => { const currentState = await GM.getValue(MAGICISRC_ENABLE_AUTO_RELOAD, true); const newState = !currentState; await GM.setValue(MAGICISRC_ENABLE_AUTO_RELOAD, newState); console.log(`[${scriptName}] MagicISRC automatic reload is now ${newState ? 'DISABLED' : 'ENABLED'}.`); await setupMenuCommands(); }); const currentHostname = location.hostname; const currentPathname = location.pathname; const configsForMenuCommands = siteConfigurations.filter(config => { if (config.autoClick) return false; const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames]; const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname)); if (hostnameMatches) { const paths = Array.isArray(config.paths) ? config.paths : [config.paths]; return paths.some(pathPattern => currentPathname.endsWith(pathPattern)); } return false; }); for (const config of configsForMenuCommands) { if (config.menuCommandName) { registerCommand(config.menuCommandName, () => { sessionStorage.setItem(SUBMISSION_TRIGGERED_FLAG, 'true'); console.log(`[${scriptName}] Menu command triggered. Stored submission state: true.`); sendMessageToChannel(config.channelName, config.messageTrigger); }); console.log(`[${scriptName}] Registered menu command "${config.menuCommandName}".`); } } const configForSuccessCheck = siteConfigurations.find(config => { const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames]; const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname)); return hostnameMatches && config.shouldCloseAfterSuccess; }); if (configForSuccessCheck && isSubmissionSuccessful(configForSuccessCheck, true)) { registerCommand(GLOBAL_CLOSE_TAB_MENU_COMMAND_NAME, () => { console.log(`[${scriptName}] Global close menu command triggered.`); sendMessageToChannel(GLOBAL_CLOSE_TAB_CHANNEL_NAME, GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER); }); console.log(`[${scriptName}] Registered global close menu command.`); } console.log(`[${scriptName}] Menu commands setup finished. Total commands: ${registeredMenuCommandIDs.length}`); } /** * Attempts to perform a MagicISRC button click if a submission is pending. * This uses a MutationObserver to wait for the button to appear. * @returns {Promise<void>} */ async function handleMagicISRCButtonInteraction() { const currentHostname = location.hostname; const currentPathname = location.pathname; const submissionFlagValue = sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG); const submissionTriggered = submissionFlagValue === 'true'; console.log(`%c[${scriptName}] handleMagicISRCButtonInteraction called. SUBMISSION_TRIGGERED_FLAG: ${submissionTriggered}`, 'color: blue; font-weight: bold;'); if (submissionTriggered) { console.log(`%c[${scriptName}] SUBMISSION_TRIGGERED_FLAG is true. Proceeding with button interaction logic.`, 'color: blue; font-weight: bold;'); const magicisrcConfig = siteConfigurations.find(config => Array.isArray(config.hostnames) ? config.hostnames.some(h => currentHostname.includes(h)) : currentHostname.includes(h) && (Array.isArray(config.paths) ? config.paths.some(p => currentPathname.endsWith(p)) : currentPathname.endsWith(p)) && config.channelName === 'magicisrc_submit_channel' ); if (magicisrcConfig) { console.log(`[${scriptName}] MagicISRC config found for button interaction. Button selector: "${magicisrcConfig.buttonSelector}"`); let buttonFoundAndClicked = false; const checkAndClickButton = async () => { const btn = document.querySelector(magicisrcConfig.buttonSelector); if (btn) { console.log(`[${scriptName}] Button found:`, btn, `Disabled state: ${btn.disabled}`); if (!btn.disabled) { console.log(`%c[${scriptName}] Button found and ENABLED. Clicking now.`, 'color: green; font-weight: bold;'); const delayMs = await getCalculatedDelay(magicisrcConfig.disableDelay); console.log(`[${scriptName}] Attempting to click MagicISRC button "${btn.textContent.trim()}" in ${delayMs / 1000} seconds.`); setTimeout(() => { btn.click(); }, delayMs); return true; } else { console.log(`[${scriptName}] Button found but is DISABLED.`); } } else { console.log(`[${scriptName}] Button not yet found.`); } return false; }; if (await checkAndClickButton()) { buttonFoundAndClicked = true; return; } console.log(`[${scriptName}] Button not immediately available/enabled. Setting up MutationObserver for button interaction.`); const observer = new MutationObserver(async (mutations, obs) => { console.log(`[${scriptName}] MutationObserver (button interaction) triggered.`); if (await checkAndClickButton()) { obs.disconnect(); buttonFoundAndClicked = true; } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true, attributes: true }); console.log(`[${scriptName}] MutationObserver active for MagicISRC button interaction.`); } else { console.warn(`[${scriptName}] document.body not available for MagicISRC button interaction observer.`); sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG); console.log(`[${scriptName}] Cleared SUBMISSION_TRIGGERED_FLAG: body not available for observer.`); return; } } else { console.warn(`[${scriptName}] MagicISRC config not found for button interaction.`); sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG); console.log(`[${scriptName}] Cleared SUBMISSION_TRIGGERED_FLAG: MagicISRC config not found.`); } } else { console.log(`[${scriptName}] Skipping button interaction logic: SUBMISSION_TRIGGERED_FLAG is ${submissionTriggered}.`); } } /** * Handles the reload logic when a MagicISRC error is detected. * This function is now primarily called by the intercepted `renderError` or `fetch`. * @returns {Promise<void>} */ async function handleMagicISRCErrorReload() { console.log(`%c[${scriptName}] handleMagicISRCErrorReload called.`, 'color: red; font-weight: bold;'); const enableReload = await GM.getValue(MAGICISRC_ENABLE_AUTO_RELOAD, true); const reloadDelayMs = await getCalculatedDelay(); if (!enableReload) { console.warn(`%c[${scriptName}] MagicISRC automatic reload is DISABLED by user setting. Skipping reload.`, 'color: orange; font-weight: bold;'); return; } console.warn(`%c[${scriptName}] Detected MagicISRC error. Reloading page in ${reloadDelayMs / 1000} seconds.`, 'color: red; font-weight: bold;'); setTimeout(() => { console.log(`[${scriptName}] Attempting location.reload() now.`); location.reload(); }, reloadDelayMs); } /** * Injects a script into the page's context to intercept window.renderError and window.fetch. */ function injectMagicISRCErrorInterceptor() { const script = document.createElement('script'); script.textContent = ` (function() { const originalRenderError = window.renderError; window.renderError = function(message, error) { if (originalRenderError) { originalRenderError.apply(this, arguments); } else { console.error('MagicISRC original renderError was not available. Message:', message, 'Error:', error); const container = document.getElementById("container"); if (container) { container.innerHTML = '<h1 class="h3">An error occurred</h1><p>' + message + '</p>'; } } console.log('Injected script: Posting MAGICISRC_ERROR_DETECTED message.'); window.postMessage({ type: 'MAGICISRC_ERROR_DETECTED', message: message, error: error ? error.toString() : 'N/A' }, window.location.origin); }; console.log('MagicISRC renderError function successfully intercepted by injected script.'); const originalFetch = window.fetch; window.fetch = function(...args) { return originalFetch.apply(this, args) .then(response => { if (!response.ok && response.status >= 500 && response.status < 600) { console.error('MagicISRC fetch intercepted: Server error detected!', response.status, response.url); console.log('Injected script: Posting MAGICISRC_NETWORK_ERROR message (server error).'); window.postMessage({ type: 'MAGICISRC_NETWORK_ERROR', url: response.url, status: response.status, statusText: response.statusText }, window.location.origin); } return response; }) .catch(error => { console.error('MagicISRC fetch intercepted: Network error detected!', error); console.log('Injected script: Posting MAGICISRC_NETWORK_ERROR message (network error).'); window.postMessage({ type: 'MAGICISRC_NETWORK_ERROR', message: error.message, error: error.toString() }, window.location.origin); throw error; }); }; console.log('MagicISRC fetch function successfully intercepted by injected script.'); })(); `; document.documentElement.appendChild(script); script.remove(); } /** * Main initialization function for the userscript. * This function focuses on setting up event listeners and core logic. */ async function initializeScript() { const currentHostname = location.hostname; const currentPathname = location.pathname; let isButtonClickInProgress = false; const isMagicISRC = currentHostname.includes('magicisrc'); if (isMagicISRC) { console.log(`[${scriptName}] MagicISRC page detected. Setting up injected script for error interception.`); injectMagicISRCErrorInterceptor(); window.addEventListener('message', async (event) => { console.log(`[${scriptName}] Received message in userscript sandbox:`, event); if (event.data && event.origin === window.location.origin) { if (event.data.type === 'MAGICISRC_ERROR_DETECTED' || event.data.type === 'MAGICISRC_NETWORK_ERROR') { console.log(`[${scriptName}] Received ${event.data.type} message from page context. Calling handleMagicISRCErrorReload.`); handleMagicISRCErrorReload(); } } }); await handleMagicISRCButtonInteraction(); } let configForSuccessCheck = null; const applicableConfigs = siteConfigurations.filter(config => { const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames]; const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname)); if (hostnameMatches) { const paths = Array.isArray(config.paths) ? config.paths : [config.paths]; return paths.some(pathPattern => currentPathname.endsWith(pathPattern)); } return false; }); configForSuccessCheck = siteConfigurations.find(config => { const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames]; const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname)); return hostnameMatches && config.shouldCloseAfterSuccess; }); await getCurrentClickDelayRange(); const disableAutoClose = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false); console.log(`[${scriptName}] Automatic tab closing is currently ${disableAutoClose ? 'DISABLED' : 'ENABLED'}.`); for (const config of applicableConfigs) { if (config.autoClick) { console.log(`[${scriptName}] Setting up AUTOMATIC button-clicking logic for a rule.`); const findAndClickButton = async () => { const btn = document.querySelector(config.buttonSelector); if (btn && !btn.disabled) { const delayMs = await getCalculatedDelay(config.disableDelay); console.log(`[${scriptName}] Automatically clicking button "${btn.textContent.trim()}" in ${delayMs / 1000} seconds.`); setTimeout(() => { btn.click(); }, delayMs); return true; } return false; }; document.addEventListener('DOMContentLoaded', () => { console.log(`[${scriptName}] DOMContentLoaded listener for autoClick button setup.`); const observer = new MutationObserver(async (mutations, obs) => { console.log(`[${scriptName}] MutationObserver (autoClick button) triggered.`); if (await findAndClickButton()) { obs.disconnect(); console.log(`[${scriptName}] Automatic button found and clicked via observer.`); } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true, attributes: true }); console.log(`[${scriptName}] MutationObserver active for automatic button click on DOMContentLoaded.`); } }); } else { if (!config.channelName || !config.messageTrigger) { console.warn(`[${scriptName}] Skipping manual button-clicking setup for a config without channelName or messageTrigger.`, config); continue; } console.log(`[${scriptName}] Setting up BROADCAST CHANNEL button-clicking logic for "${config.channelName}".`); try { const channel = new BroadcastChannel(config.channelName); channel.addEventListener('message', async (event) => { if (event.data === config.messageTrigger) { if (isSubmissionSuccessful(config)) { console.log(`[${scriptName}] Received trigger message "${event.data}" but already on a success page. Skipping button click.`); return; } sessionStorage.setItem(SUBMISSION_TRIGGERED_FLAG, 'true'); console.log(`[${scriptName}] Stored submission state: true (due to received command).`); console.log(`[${scriptName}] Received trigger message "${event.data}".`); if (isButtonClickInProgress) { console.log(`[${scriptName}] Button click already in progress. Ignoring duplicate message.`); return; } isButtonClickInProgress = true; const btn = document.querySelector(config.buttonSelector); if (btn) { const delayMs = await getCalculatedDelay(config.disableDelay); console.log(`[${scriptName}] Attempting to click button "${btn.textContent.trim()}" in ${delayMs / 1000} seconds.`); setTimeout(() => { btn.click(); isButtonClickInProgress = false; }, delayMs); } else { console.warn(`[${scriptName}] Button for selector "${JSON.stringify(config.buttonSelector)}" not found.`); isButtonClickInProgress = false; } } }); console.log(`[${scriptName}] Listener active for button clicks on channel "${config.channelName}".`); } catch (error) { console.error(`[${scriptName}] Error initializing BroadcastChannel:`, error); } } } if (configForSuccessCheck) { await checkAndCloseIfSuccessful(configForSuccessCheck); let lastUrl = location.href; const urlChangedHandler = async () => { if (location.href !== lastUrl) { console.log(`%c[${scriptName}] URL changed from "${lastUrl}" to "${location.href}". Re-checking for success.`, 'color: purple; font-weight: bold;'); lastUrl = location.href; await checkAndCloseIfSuccessful(configForSuccessCheck); if (isMagicISRC) { console.log(`[${scriptName}] Calling handleMagicISRCButtonInteraction from urlChangedHandler.`); await handleMagicISRCButtonInteraction(); } } }; window.addEventListener('popstate', urlChangedHandler); const originalPushState = history.pushState; history.pushState = function() { originalPushState.apply(this, arguments); urlChangedHandler(); }; const originalReplaceState = history.replaceState; history.replaceState = function() { originalReplaceState.apply(this, arguments); urlChangedHandler(); }; console.log(`[${scriptName}] URL change listeners activated.`); if (isSubmissionSuccessful(configForSuccessCheck)) { console.log(`[${scriptName}] Current URL matches a success pattern. Setting up global close listener.`); try { const globalCloseChannel = new BroadcastChannel(GLOBAL_CLOSE_TAB_CHANNEL_NAME); globalCloseChannel.addEventListener('message', (event) => { if (event.data === GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER) { console.log(`[${scriptName}] Received global close request.`); attemptCloseTab(50); } }); console.log(`[${scriptName}] Global close channel listener active.`); } catch (error) { console.error(`[${scriptName}] Error initializing global BroadcastChannel:`, error); } } else { console.log(`[${scriptName}] Current URL does NOT match a success pattern. Global close listener skipped.`); } } else { console.log(`[${scriptName}] No success-check config found for this hostname. Not monitoring for closure.`); sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG); } console.log(`%c[${scriptName}] Script initialization finished.`, 'font-weight: bold;'); } await setupMenuCommands(); await initializeScript(); })();