Poe Enhanced Controls

Adds quick controls for Poe conversations.

// ==UserScript==
// @name         Poe Enhanced Controls
// @namespace    https://codeberg.org/TwilightAlicorn/Poe-Enhanced-Controls
// @version      0.3.9
// @description  Adds quick controls for Poe conversations.
// @author       TwilightAlicorn
// @match        https://poe.com/*
// @grant        GM_log
// ==/UserScript==

(function() {
    'use strict';

    /******************************************
     * Configuration and Constants
     ******************************************/
    const DEBUG_MODE = false;
    const AUTO_DISABLE_CONTEXT_MANAGER = false; // Automatically disable auto-manage if enabled

    // Selectors and configuration values
    const ACTION_CONTAINER_SELECTOR = '.ChatMessageInputContainer_actionContainerLeft__dIwkm';
    const TOOLTIP_DELAY = 150;
    const MODAL_SELECTOR = '.Modal_overlay___PrHh';
    const HEADER_CLICKABLE_SELECTOR = '.ChatHeader_clickable__7fBYF';
    const POLL_INTERVAL_MS = 200; // Interval for polling the switch state

    const HIDDEN_MODAL_CLASS = 'poe-enhanced-hidden-modal';

    // Log prefixes and levels
    const NAVIGATION_PREFIX = 'Navigation';
    const OBSERVER_PREFIX = 'Observer';
    const DOM_PREFIX = 'DOM';
    const STATE_PREFIX = 'State';
    const LogLevel = {
        DEBUG: 'debug',
        INFO: 'info',
        WARN: 'warn',
        ERROR: 'error'
    };

    /******************************************
     * Global and Chat State Management
     ******************************************/
    const GlobalState = {
        controlsState: {
            isCreatingControls: false,
            controlsCreated: false  // Indicates that the button has been created
        },
        modalMonitorInterval: null,
        lastKnownSwitchState: null,
        // Will hold the observer that monitors the action container
        actionContainerObserver: null
    };

    const ChatState = {
        isAutoManageEnabled: false
    };

    // Flag for manual toggle process
    let manualToggleInProgress = false;
    // Flag to force disable auto-manage if applied automatically.
    let forcedAutoDisable = false;

    /******************************************
     * Utility Logging Function
     ******************************************/
    function log(prefix, message, level = LogLevel.INFO, data = null) {
        if (!DEBUG_MODE) return;
        const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
        const cleanPrefix = prefix.replace(/[\[\]]/g, '');
        const fullMessage = `[${cleanPrefix} ${timestamp}] ${message}`;
        if (data) {
            console[level](fullMessage, data);
        } else {
            console[level](fullMessage);
        }
    }

    /******************************************
     * Helper: debounce
     * Creates a debounced version of a function.
     ******************************************/
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => { func.apply(this, args); }, wait);
        }
    }

    /******************************************
     * Helper: openAutoManageModal
     * Emulates a click on the chat header to open a modal containing
     * the "Auto-manage context" text, then waits for the modal to appear.
     ******************************************/
    function openAutoManageModal() {
        return new Promise((resolve) => {
            // Hide modal visually to avoid flicker
            document.body.classList.add(HIDDEN_MODAL_CLASS);
            const headerButton = document.querySelector(HEADER_CLICKABLE_SELECTOR);
            if (!headerButton) {
                document.body.classList.remove(HIDDEN_MODAL_CLASS);
                resolve(null);
                return;
            }
            headerButton.click();

            const observer = new MutationObserver((mutations, obs) => {
                // Look for the modal that contains the text "Auto-manage context"
                const modal = Array.from(document.querySelectorAll(MODAL_SELECTOR))
                    .find(el => el.textContent && el.textContent.includes("Auto-manage context"));
                if (modal) {
                    obs.disconnect();
                    resolve(modal);
                }
            });

            observer.observe(document.body, { childList: true, subtree: true });
            // Timeout after 5 seconds to avoid indefinite waiting
            setTimeout(() => {
                observer.disconnect();
                document.body.classList.remove(HIDDEN_MODAL_CLASS);
                resolve(null);
            }, 5000);
        });
    }

    /******************************************
     * Function: getAutoManageState
     * Opens the modal, retrieves the auto-manage switch state, then closes the modal.
     ******************************************/
    async function getAutoManageState() {
        const modal = await openAutoManageModal();
        if (modal) {
            const switchInput = modal.querySelector('.switch_input__8I5Oq');
            const state = switchInput ? switchInput.checked : false;
            // Close the modal if possible
            const closeButton = modal.querySelector('.Modal_closeButton__GycnR');
            if (closeButton) closeButton.click();
            document.body.classList.remove(HIDDEN_MODAL_CLASS);
            return state;
        }
        return false;
    }

    /******************************************
     * Function: updateChatState
     * Updates ChatState and updates the button state accordingly.
     ******************************************/
    async function updateChatState() {
        // Let React settle
        await new Promise(r => setTimeout(r, 300));
        let state = await getAutoManageState();
        if (forcedAutoDisable) {
            state = false;
            forcedAutoDisable = false;
        }
        ChatState.isAutoManageEnabled = state;
        const autoManageButton = document.querySelector('[data-button-auto-manage]');
        if (autoManageButton) {
            if (state) {
                autoManageButton.classList.add('active');
            } else {
                autoManageButton.classList.remove('active');
            }
        }
        log(STATE_PREFIX, 'Chat state updated', LogLevel.DEBUG, ChatState);
    }

    /******************************************
     * Function: handleAutoDisableContextManager
     * Automatically disables the auto-manage context if enabled.
     ******************************************/
    async function handleAutoDisableContextManager() {
        log(STATE_PREFIX, 'Checking if auto-disable is needed', LogLevel.DEBUG);
        if (!AUTO_DISABLE_CONTEXT_MANAGER) {
            log(STATE_PREFIX, 'Auto-disable is turned off, skipping', LogLevel.DEBUG);
            return false;
        }

        const currentState = await getAutoManageState();
        if (!currentState) {
            log(STATE_PREFIX, 'Context manager is already disabled, no action needed', LogLevel.DEBUG);
            return false;
        }

        log(STATE_PREFIX, 'Context manager is enabled, proceeding to disable it', LogLevel.INFO);

        return new Promise(async (resolve) => {
            document.body.classList.add(HIDDEN_MODAL_CLASS);
            const modal = await openAutoManageModal();
            if (!modal) {
                document.body.classList.remove(HIDDEN_MODAL_CLASS);
                resolve(false);
                return;
            }
            const switchInput = modal.querySelector('.switch_input__8I5Oq');
            if (switchInput) {
                switchInput.click();
                setTimeout(() => {
                    const closeButton = modal.querySelector('.Modal_closeButton__GycnR');
                    if (closeButton) closeButton.click();
                    document.body.classList.remove(HIDDEN_MODAL_CLASS);
                    ChatState.isAutoManageEnabled = false;
                    forcedAutoDisable = true;
                    log(STATE_PREFIX, 'Auto-manage has been disabled automatically', LogLevel.INFO);
                    resolve(true);
                }, 100);
            } else {
                document.body.classList.remove(HIDDEN_MODAL_CLASS);
                resolve(false);
            }
        });
    }

    /******************************************
     * Function: monitorModalChanges
     * Polls the auto-manage switch state in the modal and updates the button accordingly.
     ******************************************/
    function monitorModalChanges(modal) {
        const switchInput = modal.querySelector('.switch_input__8I5Oq');
        if (!switchInput) return null;
        let lastState = switchInput.checked;
        return setInterval(() => {
            const currentState = switchInput.checked;
            if (currentState !== lastState) {
                lastState = currentState;
                const autoManageButton = document.querySelector('[data-button-auto-manage]');
                log(DOM_PREFIX, 'Modal manual change detected; updating button state', LogLevel.DEBUG, { isEnabled: currentState });
                if (autoManageButton) {
                    if (currentState) {
                        autoManageButton.classList.add('active');
                    } else {
                        autoManageButton.classList.remove('active');
                    }
                    ChatState.isAutoManageEnabled = currentState;
                }
            }
        }, POLL_INTERVAL_MS);
    }

    /******************************************
     * Function: handleAutoManageToggle
     * Handles manual toggle of the auto-manage switch when the button is clicked.
     ******************************************/
    async function handleAutoManageToggle(button) {
        log(DOM_PREFIX, 'Starting auto-manage toggle', LogLevel.DEBUG);
        manualToggleInProgress = true;

        // Optimistically update the button state
        const currentActive = button.classList.contains('active');
        const newState = !currentActive;
        if (newState)
            button.classList.add('active');
        else
            button.classList.remove('active');
        ChatState.isAutoManageEnabled = newState;
        GlobalState.lastKnownSwitchState = newState;

        document.body.classList.add(HIDDEN_MODAL_CLASS);
        const modal = await openAutoManageModal();
        if (!modal) {
            document.body.classList.remove(HIDDEN_MODAL_CLASS);
            setTimeout(() => { manualToggleInProgress = false; }, 500);
            return;
        }
        return new Promise((resolve) => {
            const switchInput = modal.querySelector('.switch_input__8I5Oq');
            if (switchInput) {
                switchInput.click();
                log(DOM_PREFIX, `Auto-manage ${newState ? 'enabled' : 'disabled'}`, LogLevel.INFO);
                setTimeout(() => {
                    const closeButton = modal.querySelector('.Modal_closeButton__GycnR');
                    if (closeButton) closeButton.click();
                    document.body.classList.remove(HIDDEN_MODAL_CLASS);
                    setTimeout(() => { manualToggleInProgress = false; }, 500);
                    resolve();
                }, 150);
            } else {
                document.body.classList.remove(HIDDEN_MODAL_CLASS);
                setTimeout(() => { manualToggleInProgress = false; }, 500);
                resolve();
            }
        });
    }

    /******************************************
     * Function: createTooltip
     * Creates a tooltip element for the auto-manage button.
     ******************************************/
    function createTooltip(text) {
        const tooltip = document.createElement('div');
        tooltip.className = 'custom-tooltip';
        tooltip.setAttribute('role', 'tooltip');
        tooltip.textContent = text;

        const arrow = document.createElement('div');
        arrow.className = 'custom-tooltip-arrow';
        arrow.setAttribute('data-placement', 'top');

        tooltip.appendChild(arrow);
        document.body.appendChild(tooltip);

        return tooltip;
    }

    /******************************************
     * Function: positionTooltip
     * Positions the tooltip relative to the target element.
     ******************************************/
    function positionTooltip(tooltip, targetElement) {
        const rect = targetElement.getBoundingClientRect();
        const tooltipRect = tooltip.getBoundingClientRect();

        tooltip.style.transform = `translate(${rect.left + rect.width / 2 - tooltipRect.width / 2}px, ${rect.top - tooltipRect.height - 8}px)`;

        const arrow = tooltip.querySelector('.custom-tooltip-arrow');
        arrow.style.left = `${tooltipRect.width / 2 - 3}px`;
        arrow.style.bottom = '-3px';
    }

    /******************************************
     * Function: createControls
     * Creates the auto-manage toggle button inside the chat input action container.
     * IMPORTANT: It first checks that the current URL is a chat page.
     ******************************************/
    async function createControls() {
        // Only run if URL indicates a chat ("/chat/")
        if (!window.location.pathname.includes('/chat/')) {
            log(DOM_PREFIX, 'Not a chat page; skipping createControls', LogLevel.DEBUG);
            GlobalState.controlsState.isCreatingControls = false;
            return;
        }

        const actionContainer = document.querySelector(ACTION_CONTAINER_SELECTOR);
        if (!actionContainer) {
            log(DOM_PREFIX, 'Action container not found; cannot create controls', LogLevel.WARN);
            return;
        }
        // Prevent duplicate creation if button already exists
        if (actionContainer.querySelector('[data-button-auto-manage]')) {
            log(DOM_PREFIX, 'Button already exists in container; skipping creation', LogLevel.DEBUG);
            return;
        }

        // Skip if another creation process is in progress
        if (GlobalState.controlsState.isCreatingControls) {
            log(DOM_PREFIX, 'Creation in progress; skipping duplicate call', LogLevel.DEBUG);
            return;
        }
        GlobalState.controlsState.isCreatingControls = true;

        // Call auto-disable procedure (if configured) then update UI state.
        const wasDisabled = await handleAutoDisableContextManager();
        if (wasDisabled) {
            ChatState.isAutoManageEnabled = false;
            const autoManageButton = document.querySelector('[data-button-auto-manage]');
            if (autoManageButton) autoManageButton.classList.remove('active');
        } else {
            await updateChatState();
        }

        // Create the auto-manage button
        const autoManageButton = document.createElement('button');
        autoManageButton.className = 'button_root__TL8nv button_ghost__YsMI5 button_sm__hWzjK button_center__RsQ_o button_showIconOnly-always__05Gb5';
        autoManageButton.setAttribute('type', 'button');
        autoManageButton.setAttribute('aria-label', 'Auto-manage Context');
        autoManageButton.setAttribute('data-button-auto-manage', 'true');

        if (ChatState.isAutoManageEnabled)
            autoManageButton.classList.add('active');

        log(DOM_PREFIX, 'Initial button state:', LogLevel.DEBUG, {
            isAutoManageEnabled: ChatState.isAutoManageEnabled,
            hasActiveClass: autoManageButton.classList.contains('active')
        });

        autoManageButton.innerHTML = `
            <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg" style="height:18px; width:18px; display:block; flex:0 0 auto;">
                <path fill="currentColor" d="M5.667 3.167c0-.169-.11-.315-.27-.363l-1.381-.486-.48-1.357a.377.377 0 0 0-.37-.293c-.178 0-.328.12-.358.26l-.49 1.39-1.359.48a.376.376 0 0 0-.292.37c0 .177.121.328.26.357l1.391.49.48 1.357c.04.175.191.295.369.295.177 0 .329-.121.359-.26l.49-1.391 1.358-.481a.374.374 0 0 0 .293-.368ZM11.75 17.083a2.102 2.102 0 0 1-1.405-1.416H8.35l-.643-1.93a.831.831 0 0 0-1.58 0l-.644 1.93h-.924c.237-.519.487-1.164.666-1.882a14.6 14.6 0 0 0 .332-1.868h6.053c.038.331.096.698.166 1.075l.01-.003.887-.314.328-.928c.156-.53.524-.963 1-1.225V8.583a.833.833 0 0 0-.833-.833h-2.5V2.333c0-1.149-.935-2.083-2.084-2.083S6.5 1.184 6.5 2.333V7.75H4a.833.833 0 0 0-.833.833v2.5c0 .418.311.748.711.81-.058.462-.141.974-.27 1.489a9.626 9.626 0 0 1-1.135 2.656.834.834 0 0 0 .694 1.295h2.916c.36 0 .677-.229.79-.57l.044-.128.043.128c.113.341.43.57.79.57h4v-.25ZM8.166 2.333c0-.229.187-.416.416-.416.23 0 .417.187.417.416V7.75h-.833V2.333ZM4.833 9.417h7.5v.833h-7.5v-.833Z" />
                <path fill="currentColor" d="M18.167 15.043a.472.472 0 0 0-.338-.455l-1.726-.607-.601-1.697a.47.47 0 0 0-.461-.367c-.223 0-.41.15-.448.325l-.613 1.738-1.697.6a.47.47 0 0 0-.366.462c0 .221.151.41.326.447l1.738.613.6 1.696a.468.468 0 0 0 .46.367c.222 0 .412-.15.45-.326l.612-1.738L17.8 15.5a.465.465 0 0 0 .367-.457Z" />
            </svg>
            <span class="button_label__mCaDf"></span>
        `;

        // Insert the auto-manage button into the action container.
        // Prefer putting it after the clear context button if available.
        const clearContextButton = actionContainer.querySelector('[data-button-chat-break]');
        if (clearContextButton) {
            clearContextButton.after(autoManageButton);
        } else {
            actionContainer.appendChild(autoManageButton);
        }

        // Create tooltip and attach events
        const tooltip = createTooltip('Auto-manage Context');
        let tooltipTimeout;
    
        autoManageButton.addEventListener('mouseenter', () => {
            clearTimeout(tooltipTimeout);
            tooltipTimeout = setTimeout(() => {
                positionTooltip(tooltip, autoManageButton);
                tooltip.classList.add('visible');
            }, TOOLTIP_DELAY);
        });
    
        autoManageButton.addEventListener('mouseleave', () => {
            clearTimeout(tooltipTimeout);
            tooltip.classList.remove('visible');
        });
    
        autoManageButton.addEventListener('click', function() {
            handleAutoManageToggle(this);
        });
    
        // Mark creation as complete
        GlobalState.controlsState.controlsCreated = true;
        GlobalState.controlsState.isCreatingControls = false;
    }

    /******************************************
     * Function: attachActionContainerObserver
     * This observer is active while on a chat page. It checks for the 
     * existence of the action container and the auto-manage button.
     * If the container exists and the button is missing, it triggers creation.
     ******************************************/
    function attachActionContainerObserver() {
        // Only attach if on a chat page
        if (!window.location.pathname.includes('/chat/')) {
            log(OBSERVER_PREFIX, 'Not on a chat page; action container observer not attached', LogLevel.INFO);
            if (GlobalState.actionContainerObserver) {
                GlobalState.actionContainerObserver.disconnect();
                GlobalState.actionContainerObserver = null;
            }
            return;
        }
    
        if (GlobalState.actionContainerObserver) return; // Already attached
        log(OBSERVER_PREFIX, 'Attaching action container observer', LogLevel.INFO);
        GlobalState.actionContainerObserver = new MutationObserver((mutations) => {
            // Only proceed if we are on a chat page (guard clause)
            if (!window.location.pathname.includes('/chat/')) return;
    
            const actionContainer = document.querySelector(ACTION_CONTAINER_SELECTOR);
            if (!actionContainer) return;
            // If the auto-manage button is not present, create it
            if (!actionContainer.querySelector('[data-button-auto-manage]')) {
                log(DOM_PREFIX, 'Button was removed, allowing recreation', LogLevel.INFO);
                GlobalState.controlsState.controlsCreated = false;
                debounce(createControls, 300)();
            }
        });
        GlobalState.actionContainerObserver.observe(document.body, { childList: true, subtree: true });
    }

    /******************************************
     * Function: handleModalStateChange
     * Monitors modal open/close events and attaches/detaches the modal monitor.
     ******************************************/
    function handleModalStateChange() {
        if (manualToggleInProgress) {
            log(DOM_PREFIX, 'Skipping modal state update due to manual toggle in progress', LogLevel.DEBUG);
            return;
        }
    
        const isModalOpen = document.body.classList.contains('ReactModal__Body--open');
        if (!isModalOpen) {
            if (GlobalState.modalMonitorInterval) {
                clearInterval(GlobalState.modalMonitorInterval);
                GlobalState.modalMonitorInterval = null;
                log(DOM_PREFIX, 'Modal closed: disconnected modal monitor', LogLevel.INFO);
            }
            return;
        }
    
        const modal = Array.from(document.querySelectorAll(MODAL_SELECTOR))
            .find(el => el.textContent && el.textContent.includes("Auto-manage context"));
        if (!modal) {
            log(DOM_PREFIX, 'Modal open but content not found', LogLevel.DEBUG);
            return;
        }
    
        if (!GlobalState.modalMonitorInterval) {
            GlobalState.modalMonitorInterval = monitorModalChanges(modal);
            log(DOM_PREFIX, 'Attached modal monitor for manual changes', LogLevel.INFO);
        }
    }

    /******************************************
     * Function: init
     * Initializes the userscript: injects styles, sets up observers and navigation listeners.
     ******************************************/
    function init() {
        log('Init', 'Starting initialization', LogLevel.INFO);
    
        // Inject styles once
        if (!document.getElementById('poe-enhanced-styles')) {
            const styleSheet = document.createElement('style');
            styleSheet.id = 'poe-enhanced-styles';
            styleSheet.textContent = `
                [data-button-auto-manage] {
                    color: var(--pdl-comp-button-theme-ghost-fg);
                    transition: var(--pdl-comp-button-transition);
                    position: relative;
                }
                [data-button-auto-manage]:hover {
                    background-color: var(--pdl-comp-button-theme-ghost-hover-bg);
                }
                [data-button-auto-manage].active {
                    color: var(--pdl-accent-on-accent);
                    background-color: var(--pdl-accent-base);
                }
                [data-button-auto-manage].active:hover {
                    background-color: var(--pdl-accent-emphasis);
                }
                .custom-tooltip {
                    --pdl-tooltip-arrow-bg: var(--pdl-comp-tooltip-theme-base-bg);
                    --pdl-tooltip-color: var(--pdl-comp-tooltip-theme-base-fg);
                    --pdl-tooltip-bg: var(--pdl-comp-tooltip-theme-base-bg);
                    position: absolute;
                    z-index: 10000;
                    display: block;
                    font: var(--pdl-comp-tooltip-font);
                    padding: var(--pdl-comp-tooltip-padding-y) var(--pdl-comp-tooltip-padding-x);
                    border-radius: var(--pdl-comp-tooltip-border-radius);
                    max-width: var(--pdl-comp-tooltip-max-width);
                    color: var(--pdl-tooltip-color);
                    background-color: var(--pdl-tooltip-bg);
                    opacity: 0;
                    pointer-events: none;
                    will-change: transform;
                    transition: opacity 0.1s;
                }
                .custom-tooltip.visible {
                    opacity: 1;
                }
                .custom-tooltip-arrow {
                    position: absolute;
                    width: var(--pdl-comp-tooltip-arrow-width);
                    height: var(--pdl-comp-tooltip-arrow-width);
                    background: inherit;
                    visibility: hidden;
                }
                .custom-tooltip-arrow::before {
                    position: absolute;
                    width: var(--pdl-comp-tooltip-arrow-width);
                    height: var(--pdl-comp-tooltip-arrow-width);
                    background: var(--pdl-tooltip-arrow-bg);
                    visibility: visible;
                    content: '';
                    transform: rotate(45deg);
                }
                .${HIDDEN_MODAL_CLASS} .Modal_overlay___PrHh {
                    opacity: 0 !important;
                    pointer-events: none !important;
                }
            `;
            document.head.appendChild(styleSheet);
            log('Init', 'Styles injected', LogLevel.DEBUG);
        }
    
        // Observer for body changes to catch modal open/close events.
        const bodyObserver = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                    handleModalStateChange();
                }
            });
        });
        bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
        log('Init', 'Body observer started', LogLevel.DEBUG);
    
        // Override history methods to capture SPA navigation events
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;
    
        history.pushState = function() {
            log(NAVIGATION_PREFIX, 'pushState detected', LogLevel.INFO, {
                from: window.location.pathname,
                to: arguments[2]
            });
            originalPushState.apply(this, arguments);
            // Reset state on navigation
            GlobalState.controlsState.isCreatingControls = false;
            GlobalState.controlsState.controlsCreated = false;
            if (GlobalState.modalMonitorInterval) {
                clearInterval(GlobalState.modalMonitorInterval);
                GlobalState.modalMonitorInterval = null;
            }
            // Reattach the action container observer (if applicable)
            setTimeout(() => { attachActionContainerObserver(); }, 300);
        };
    
        history.replaceState = function() {
            log(NAVIGATION_PREFIX, 'replaceState detected', LogLevel.INFO, {
                from: window.location.pathname,
                to: arguments[2]
            });
            originalReplaceState.apply(this, arguments);
            GlobalState.controlsState.isCreatingControls = false;
            GlobalState.controlsState.controlsCreated = false;
            if (GlobalState.modalMonitorInterval) {
                clearInterval(GlobalState.modalMonitorInterval);
                GlobalState.modalMonitorInterval = null;
            }
            setTimeout(() => { attachActionContainerObserver(); }, 300);
        };
    
        window.addEventListener('popstate', () => {
            log(NAVIGATION_PREFIX, 'popstate detected', LogLevel.INFO, {
                from: window.location.pathname,
                to: window.location.pathname
            });
            GlobalState.controlsState.isCreatingControls = false;
            GlobalState.controlsState.controlsCreated = false;
            if (GlobalState.modalMonitorInterval) {
                clearInterval(GlobalState.modalMonitorInterval);
                GlobalState.modalMonitorInterval = null;
            }
            setTimeout(() => { attachActionContainerObserver(); }, 300);
        });
    
        // Initially set up the action container observer
        attachActionContainerObserver();
    }
    
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址