YouTube Speed and Loop

Enhances YouTube with playback speeds and repeat functionality.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name               YouTube Speed and Loop
// @name:zh-TW         YouTube 播放速度與循環
// @namespace          https://github.com/Hank8933
// @version            1.1.1
// @description        Enhances YouTube with playback speeds and repeat functionality.
// @description:zh-TW  為 YouTube 提供超過 2 倍的播放速度控制和重複播放功能。
// @author             Hank8933
// @homepage           https://github.com/Hank8933/YouTube-Speed-and-Loop
// @match              https://www.youtube.com/*
// @grant              none
// @license            MIT
// ==/UserScript==

(function () {
    'use strict';

    const PANEL_ID = 'yt-enhancements-panel';
    let isInitializing = false;

    const panelCSS = `
        :root {
            --primary-bg: transparent; --hover-bg: rgba(255, 255, 255, 0.1); --active-bg: #f00;
            --panel-bg: #282828; --text-color: #fff; --shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
            --input-bg: rgba(0, 0, 0, 0.3); --input-border: rgba(255, 255, 255, 0.2);
        }
        .yt-custom-control-panel {
            position: relative; z-index: 99999; font-family: Roboto, Arial, sans-serif;
            align-self: center; margin-right: 8px;
        }
        .yt-custom-control-toggle {
            background-color: var(--primary-bg); color: var(--text-color);
            border: 1px solid rgba(255, 255, 255, 0.1); font-weight: 500; cursor: pointer;
            transition: background-color 0.3s; display: flex; align-items: center; justify-content: center;
            width: 40px; height: 40px; box-sizing: border-box; border-radius: 50%;
            font-size: 2rem; line-height: 0;
        }
        .yt-custom-control-toggle:hover { background-color: var(--hover-bg); }
        .yt-custom-control-content {
            position: absolute; top: calc(100% + 10px); right: 0; transform: none; left: auto;
            background-color: var(--panel-bg); color: var(--text-color); padding: 12px;
            border: 1px solid var(--input-border); border-radius: 12px; box-shadow: var(--shadow);
            display: none; flex-direction: column; gap: 12px; min-width: 320px; white-space: nowrap;
        }
        .yt-custom-control-panel.expanded .yt-custom-control-content { display: flex; }
        .yt-custom-control-title {
            font-weight: bold; margin-bottom: 8px; padding: 0 5px; font-size: 16px;
        }
        .yt-custom-control-section { padding: 8px; border-radius: 8px; transition: background-color 0.2s; }
        .yt-custom-control-section:hover { background-color: rgba(255, 255, 255, 0.05); }
        .yt-custom-btn {
            background-color: rgba(255, 255, 255, 0.15); border: none; color: var(--text-color);
            padding: 6px 12px; border-radius: 18px; cursor: pointer; font-size: 13px;
            white-space: nowrap; text-align: center; flex-grow: 1; margin-right: 8px;
        }
        .yt-custom-btn:last-child { margin-right: 0; }
        .yt-custom-btn:hover { background-color: rgba(255, 255, 255, 0.25); }
        .yt-custom-btn.active { background-color: var(--active-bg); }
        .yt-custom-btn-group { display: flex; justify-content: space-between; padding: 0 8px; }
        .yt-speed-controls { display: flex; flex-direction: column; gap: 8px; white-space: nowrap; }
        .yt-slider-row { display: flex; align-items: center; width: 100%; }
        .yt-custom-slider { flex-grow: 1; min-width: 100px; }
        .yt-preset-speeds { display: flex; gap: 5px; width: 100%; }
        .loop-input-container {
            display: flex; align-items: center; justify-content: space-between;
            gap: 8px; margin-top: 10px; padding: 0 8px;
        }
        .loop-time-input {
            width: 100%; background-color: var(--input-bg);
            border: 1px solid var(--input-border); color: var(--text-color);
            border-radius: 8px; padding: 8px; font-family: 'Courier New', Courier, monospace;
            font-size: 14px; text-align: center; transition: border-color 0.3s, box-shadow 0.3s;
        }
        .loop-time-input:focus {
            outline: none; border-color: #3ea6ff;
            box-shadow: 0 0 5px rgba(62, 166, 255, 0.5);
        }
        .yt-custom-row {
            display: flex; justify-content: space-between; align-items: center;
            gap: 10px; padding: 4px 8px;
        }
        .yt-custom-row:not(:last-child) { margin-bottom: 8px; }
        .yt-custom-label {
            font-size: 14px; flex-shrink: 0;
        }
        .yt-custom-row .shortcut-label {
            flex-basis: 120px; /* Fixed width for alignment */
            text-align: right;
        }
        .yt-custom-toggle-btn {
            flex-grow: 0; min-width: 60px; margin-right: 0;
        }
        .shortcut-input {
            flex-grow: 1; /* Takes remaining space */
            background-color: var(--input-bg);
            border: 1px solid var(--input-border); color: var(--text-color);
            border-radius: 8px; padding: 8px; font-family: 'Courier New', Courier, monospace;
            font-size: 14px; text-align: center; cursor: pointer;
        }
        .shortcut-input:focus {
            outline: none; border-color: #3ea6ff;
            box-shadow: 0 0 5px rgba(62, 166, 255, 0.5);
        }
    `;
    const styleEl = document.createElement('style');
    styleEl.textContent = panelCSS;
    document.head.appendChild(styleEl);

    function getFormattedTimestamp() {
        const now = new Date();
        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        const seconds = String(now.getSeconds()).padStart(2, '0');
        return `${hours}:${minutes}:${seconds}`;
    }

    function createElement(tag, id, className, textContent) {
        const el = document.createElement(tag);
        if (id) el.id = id;
        if (className) el.className = className;
        if (textContent) el.textContent = textContent;
        return el;
    }

    let playbackRateDisconnect = () => { };
    let loopDisconnect = () => { };

    function cleanUpVideoFeatures() {
        playbackRateDisconnect();
        loopDisconnect();
        AutoConfirmController.stop();
        playbackRateDisconnect = () => { };
        loopDisconnect = () => { };
    }

    function createAndSetupControlPanel(container) {
        const panel = createElement('div', PANEL_ID, 'yt-custom-control-panel');
        const toggleBtn = createElement('button', null, 'yt-custom-control-toggle', '≡');
        const contentDiv = createElement('div', null, 'yt-custom-control-content');
        toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); panel.classList.toggle('expanded'); toggleBtn.textContent = panel.classList.contains('expanded') ? '×' : '≡'; });
        document.addEventListener('click', () => { if (panel.classList.contains('expanded')) { panel.classList.remove('expanded'); toggleBtn.textContent = '≡'; } });
        contentDiv.addEventListener('click', (e) => e.stopPropagation());

        const titleDiv = createElement('div', null, 'yt-custom-control-title', 'YouTube Enhanced Controls');

        const speedSection = createElement('div', null, 'yt-custom-control-section');
        const speedText = createElement('div', null, 'yt-custom-control-title', 'Playback Speed: ');
        const speedValue = createElement('span', null, null, '1.0');
        speedText.appendChild(speedValue); speedText.append('x');
        const speedControls = createElement('div', null, 'yt-speed-controls');
        const sliderRow = createElement('div', null, 'yt-slider-row');
        const speedSlider = createElement('input', null, 'yt-custom-slider');
        speedSlider.type = 'range'; speedSlider.min = '0.25'; speedSlider.max = '5'; speedSlider.step = '0.25'; speedSlider.value = '1';
        sliderRow.appendChild(speedSlider);
        const presetSpeeds = createElement('div', null, 'yt-preset-speeds yt-custom-btn-group');
        [1, 1.5, 2, 3, 4, 5].forEach(speed => { const btn = createElement('button', null, 'yt-custom-btn yt-speed-preset', `${speed}x`); btn.dataset.speed = speed; presetSpeeds.appendChild(btn); });
        speedControls.append(sliderRow, presetSpeeds);
        speedSection.append(speedText, speedControls);

        const loopSection = createElement('div', null, 'yt-custom-control-section');
        loopSection.appendChild(createElement('div', null, 'yt-custom-control-title', 'Loop Controls'));
        const loopToggleRow = createElement('div', null, 'yt-custom-row');
        loopToggleRow.appendChild(createElement('label', null, 'yt-custom-label', 'Loop Playback'));
        const loopToggle = createElement('button', null, 'yt-custom-btn yt-custom-toggle-btn', 'Off');
        loopToggleRow.appendChild(loopToggle);
        const rangeButtons = createElement('div', null, 'yt-custom-btn-group');
        const loopStartBtn = createElement('button', null, 'yt-custom-btn', 'Set Start');
        const loopEndBtn = createElement('button', null, 'yt-custom-btn', 'Set End');
        const loopClearBtn = createElement('button', null, 'yt-custom-btn', 'Clear');
        rangeButtons.append(loopStartBtn, loopEndBtn, loopClearBtn);
        const loopInputContainer = createElement('div', null, 'loop-input-container');
        const loopStartInput = createElement('input', null, 'loop-time-input');
        loopStartInput.type = 'text'; loopStartInput.placeholder = '00:00.000';
        const loopInputSeparator = createElement('span', null, null, '→');
        const loopEndInput = createElement('input', null, 'loop-time-input');
        loopEndInput.type = 'text'; loopEndInput.placeholder = '00:00.000';
        loopInputContainer.append(loopStartInput, loopInputSeparator, loopEndInput);
        loopSection.append(loopToggleRow, rangeButtons, loopInputContainer);

        const shortcutSection = createElement('div', null, 'yt-custom-control-section');
        shortcutSection.appendChild(createElement('div', null, 'yt-custom-control-title', 'Keyboard Shortcuts'));

        const createShortcutRow = (label, id) => {
            const row = createElement('div', null, 'yt-custom-row');
            row.appendChild(createElement('label', null, 'yt-custom-label shortcut-label', label));
            const input = createElement('input', id, 'shortcut-input');
            input.type = 'text'; input.placeholder = 'Click and press a key'; input.readOnly = true;
            row.appendChild(input);
            return { row, input };
        };

        const { row: setStartRow, input: shortcutSetStartInput } = createShortcutRow('Set Start:', 'shortcut-set-start');
        const { row: replayStartRow, input: shortcutReplayStartInput } = createShortcutRow('Jump to Start:', 'shortcut-replay-start');
        const { row: setEndRow, input: shortcutSetEndInput } = createShortcutRow('Set End:', 'shortcut-set-end');
        const { row: clearLoopRow, input: shortcutClearLoopInput } = createShortcutRow('Clear Loop:', 'shortcut-clear-loop');

        shortcutSection.append(setStartRow, replayStartRow, setEndRow, clearLoopRow);

        const autoConfirmSection = createElement('div', null, 'yt-custom-control-section');
        const autoConfirmRow = createElement('div', null, 'yt-custom-row');
        autoConfirmRow.appendChild(createElement('label', null, 'yt-custom-label', 'Auto-Click "Continue watching?"'));
        const autoConfirmToggle = createElement('button', null, 'yt-custom-btn yt-custom-toggle-btn', 'Off');
        autoConfirmRow.appendChild(autoConfirmToggle);
        autoConfirmSection.appendChild(autoConfirmRow);

        contentDiv.append(titleDiv, speedSection, loopSection, shortcutSection, autoConfirmSection);
        panel.append(toggleBtn, contentDiv);
        container.prepend(panel);

        return {
            speedSection, speedValue, speedSlider, presetSpeeds,
            loopSection, loopToggle,
            loopStartBtn, loopEndBtn, loopClearBtn, loopStartInput, loopEndInput,
            shortcutSetStartInput, shortcutReplayStartInput, shortcutSetEndInput, shortcutClearLoopInput,
            autoConfirmToggle
        };
    }

    function waitForElement(selector, parent = document) {
        return new Promise(resolve => {
            const interval = setInterval(() => {
                const element = parent.querySelector(selector);
                if (element) {
                    clearInterval(interval);
                    resolve(element);
                }
            }, 250);
        });
    }

    const SpeedController = {
        updatePlaybackRate(rate, elements) {
            if (!document.querySelector('video') || !elements) return;
            elements.speedValue.textContent = parseFloat(rate).toFixed(2);
            elements.speedSlider.value = rate;
            elements.presetSpeeds.querySelectorAll('.yt-speed-preset').forEach(btn => {
                btn.classList.toggle('active', parseFloat(btn.dataset.speed) === parseFloat(rate));
            });
        },
        init(video, elements) {
            elements.speedSlider.addEventListener('input', () => { video.playbackRate = parseFloat(elements.speedSlider.value); this.updatePlaybackRate(video.playbackRate, elements); });
            elements.presetSpeeds.addEventListener('click', (e) => { const btn = e.target.closest('.yt-speed-preset'); if (btn) { video.playbackRate = parseFloat(btn.dataset.speed); this.updatePlaybackRate(video.playbackRate, elements); } });
            let lastRate = video.playbackRate;
            const observer = setInterval(() => { const cv = document.querySelector('video'); if (cv && cv.playbackRate !== lastRate) { lastRate = cv.playbackRate; this.updatePlaybackRate(lastRate, elements); } }, 500);
            playbackRateDisconnect = () => clearInterval(observer);
        }
    };

    const LoopController = {
        loopStart: null,
        loopEnd: null,
        formatTime(seconds) { if (seconds === null || isNaN(seconds)) return ''; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); const ms = Math.round((seconds - Math.floor(seconds)) * 1000); return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}.${String(ms).padStart(3, '0')}`; },
        parseTime(timeStr) { if (!timeStr) return null; const parts = timeStr.split(':'); let seconds = 0; try { if (parts.length === 2) { seconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]); } else { seconds = parseFloat(parts[0]); } return isNaN(seconds) ? null : seconds; } catch (e) { return null; } },
        init(video, elements) {
            let isLooping = video.loop;
            const { loopToggle, loopStartBtn, loopEndBtn, loopClearBtn, loopStartInput, loopEndInput } = elements;
            const updateLoopInputs = () => { loopStartInput.value = this.formatTime(this.loopStart); loopEndInput.value = this.formatTime(this.loopEnd); };
            const updateLoopState = (newState) => { isLooping = newState; loopToggle.textContent = isLooping ? 'On' : 'Off'; loopToggle.classList.toggle('active', isLooping); };
            updateLoopState(isLooping);
            updateLoopInputs();
            loopToggle.addEventListener('click', () => { video.loop = !video.loop; updateLoopState(video.loop); });
            loopStartBtn.addEventListener('click', () => { this.loopStart = video.currentTime; updateLoopInputs(); });
            loopEndBtn.addEventListener('click', () => { this.loopEnd = video.currentTime; updateLoopInputs(); });
            loopClearBtn.addEventListener('click', () => { this.loopStart = null; this.loopEnd = null; updateLoopInputs(); });
            loopStartInput.addEventListener('change', () => { const parsed = this.parseTime(loopStartInput.value); this.loopStart = parsed; loopStartInput.value = this.formatTime(parsed); });
            loopEndInput.addEventListener('change', () => { const parsed = this.parseTime(loopEndInput.value); this.loopEnd = parsed; loopEndInput.value = this.formatTime(parsed); });
            video.addEventListener('timeupdate', () => { if (isLooping && this.loopStart !== null && this.loopEnd !== null && this.loopStart < this.loopEnd && video.currentTime >= this.loopEnd) { video.currentTime = this.loopStart; } });
            let lastLoopState = video.loop;
            const observer = setInterval(() => { const cv = document.querySelector('video'); if (cv && cv.loop !== lastLoopState) { lastLoopState = cv.loop; updateLoopState(lastLoopState); } }, 500);
            loopDisconnect = () => clearInterval(observer);
        }
    };

    const AutoConfirmController = {
        observer: null,
        isEnabled: false,
        storageKey: 'yt-auto-confirm-enabled',
        init(toggleButton) {
            const savedState = localStorage.getItem(this.storageKey);
            this.isEnabled = savedState === 'true';
            this.updateButtonState(toggleButton);
            if (this.isEnabled) this.start();
            toggleButton.addEventListener('click', () => { this.isEnabled = !this.isEnabled; localStorage.setItem(this.storageKey, this.isEnabled); this.updateButtonState(toggleButton); this.isEnabled ? this.start() : this.stop(); });
        },
        updateButtonState(toggleButton) {
            if (toggleButton) { toggleButton.textContent = this.isEnabled ? 'On' : 'Off'; toggleButton.classList.toggle('active', this.isEnabled); }
        },
        start() {
            if (this.observer) return;
            this.observer = new MutationObserver(() => {
                const dialog = document.querySelector('yt-confirm-dialog-renderer');
                if (dialog && dialog.offsetParent !== null) {
                    console.log(`%c[YouTube Enhanced Controls]%c [${getFormattedTimestamp()}] Auto-clicked "Continue Watching?" dialog.`, 'font-weight: bold; color: #ff8c00;', 'color: inherit;');
                    dialog.querySelector('#confirm-button')?.click();
                }
            });
            this.observer.observe(document.body, { childList: true, subtree: true });
        },
        stop() {
            if (this.observer) { this.observer.disconnect(); this.observer = null; }
        }
    };

    const ShortcutController = {
        config: {},
        storageKey: 'yt-speed-loop-shortcuts',
        elements: {},
        actions: ['setStart', 'replayStart', 'setEnd', 'clearLoop'],
        init(uiElements) {
            this.elements = {
                setStart: uiElements.shortcutSetStartInput,
                replayStart: uiElements.shortcutReplayStartInput,
                setEnd: uiElements.shortcutSetEndInput,
                clearLoop: uiElements.shortcutClearLoopInput,
                loopStartInput: uiElements.loopStartInput,
                loopEndInput: uiElements.loopEndInput
            };
            this.loadConfig();
            this.updateAllInputs();
            this.actions.forEach(action => {
                const element = this.elements[action];
                if (element) {
                    element.addEventListener('keydown', e => this.captureShortcut(e, action));
                    element.addEventListener('blur', () => this.updateInput(action));
                }
            });
            document.addEventListener('keydown', e => this.handleGlobalKeyDown(e));
        },
        loadConfig() { try { const savedConfig = localStorage.getItem(this.storageKey); this.config = savedConfig ? JSON.parse(savedConfig) : {}; } catch (e) { console.error('[YouTube Enhanced Controls] Error loading shortcut config:', e); this.config = {}; } },
        saveConfig() { localStorage.setItem(this.storageKey, JSON.stringify(this.config)); },
        updateInput(action) { const keyConfig = this.config[action]; const inputEl = this.elements[action]; if (!inputEl) return; inputEl.value = keyConfig ? this.formatKey(keyConfig) : ''; if (!inputEl.value) inputEl.placeholder = 'Click and press a key'; },
        updateAllInputs() { this.actions.forEach(action => this.updateInput(action)); },
        formatKey(keyEvent) { if (!keyEvent || !keyEvent.key) return ''; const parts = []; if (keyEvent.ctrlKey) parts.push('Ctrl'); if (keyEvent.altKey) parts.push('Alt'); if (keyEvent.shiftKey) parts.push('Shift'); const key = keyEvent.key.trim(); if (key && !['Control', 'Alt', 'Shift', 'Meta'].includes(keyEvent.key)) { parts.push(key.length === 1 ? key.toUpperCase() : key); } return parts.join(' + '); },
        captureShortcut(event, action) {
            event.preventDefault(); event.stopPropagation();
            if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return;
            const newConfig = { key: event.key, code: event.code, ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey };
            this.config[action] = newConfig;
            this.elements[action].value = this.formatKey(newConfig);
            this.saveConfig();
            this.elements[action].blur();
        },
        handleGlobalKeyDown(event) {
            if (['INPUT', 'TEXTAREA'].includes(event.target.tagName) || event.target.isContentEditable) return;
            for (const action in this.config) {
                const savedKey = this.config[action];
                if (savedKey && savedKey.key === event.key && savedKey.ctrlKey === event.ctrlKey && savedKey.altKey === event.altKey && savedKey.shiftKey === event.shiftKey) { event.preventDefault(); event.stopPropagation(); this.executeAction(action); return; }
            }
        },
        executeAction(action) {
            const video = document.querySelector('video');
            if (!video) return;
            switch (action) {
                case 'setStart':
                    LoopController.loopStart = video.currentTime;
                    if (this.elements.loopStartInput) this.elements.loopStartInput.value = LoopController.formatTime(LoopController.loopStart);
                    break;
                case 'replayStart':
                    if (LoopController.loopStart !== null) video.currentTime = LoopController.loopStart;
                    break;
                case 'setEnd':
                    LoopController.loopEnd = video.currentTime;
                    if (this.elements.loopEndInput) this.elements.loopEndInput.value = LoopController.formatTime(LoopController.loopEnd);
                    break;
                case 'clearLoop':
                    LoopController.loopStart = null;
                    LoopController.loopEnd = null;
                    if (this.elements.loopStartInput) this.elements.loopStartInput.value = LoopController.formatTime(null);
                    if (this.elements.loopEndInput) this.elements.loopEndInput.value = LoopController.formatTime(null);
                    break;
            }
        }
    };

    async function init() {
        if (isInitializing) return;
        isInitializing = true;
        try {
            document.getElementById(PANEL_ID)?.remove();
            cleanUpVideoFeatures();
            const anchorElement = await waitForElement('ytd-masthead #end #buttons #avatar-btn, ytd-masthead #end #buttons ytd-button-renderer');
            const buttonsContainer = anchorElement.closest('#buttons');
            if (!buttonsContainer) { console.error('[YouTube Enhanced Controls] Could not find #buttons container.'); return; }

            const panelElements = createAndSetupControlPanel(buttonsContainer);
            AutoConfirmController.init(panelElements.autoConfirmToggle);
            ShortcutController.init(panelElements);

            const videoContainer = document.querySelector('ytd-watch-flexy');
            const isWatchPage = !!videoContainer;
            panelElements.speedSection.style.display = isWatchPage ? 'block' : 'none';
            panelElements.loopSection.style.display = isWatchPage ? 'block' : 'none';
            document.querySelectorAll('.shortcut-input').forEach(el => { el.closest('.yt-custom-row').style.display = isWatchPage ? 'flex' : 'none'; });

            if (isWatchPage) {
                try {
                    const video = await waitForElement('video', videoContainer);
                    if (video.paused && video.currentTime < 3 && AutoConfirmController.isEnabled && document.hidden) {
                        console.log(`%c[YouTube Enhanced Controls]%c [${getFormattedTimestamp()}] Proactively playing video in background...`, 'font-weight: bold; color: #ff8c00;', 'color: inherit;');
                        video.play().catch(e => console.warn(`%c[YouTube Enhanced Controls]%c Proactive play failed:`, 'font-weight: bold; color: #ff8c00;', e));
                    }
                    SpeedController.init(video, panelElements);
                    LoopController.init(video, panelElements);
                    SpeedController.updatePlaybackRate(video.playbackRate, panelElements);
                } catch (error) {
                    panelElements.speedSection.style.display = 'none';
                    panelElements.loopSection.style.display = 'none';
                }
            }
        } finally { isInitializing = false; }
    }

    document.addEventListener('yt-navigate-finish', init);
    waitForElement('title').then(titleElement => { new MutationObserver(init).observe(titleElement, { childList: true }); });
})();