Pages View Google Docs

Adds a thumbnail view similar to MS Word, including both heading and section grouping

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Pages View Google Docs
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Adds a thumbnail view similar to MS Word, including both heading and section grouping
// @match        https://docs.google.com/document/d/*
// @license      MIT
// @grant        none
// ==/UserScript==


(() => {
    'use strict';

    // ---------------------------
    // 1. Utility Functions
    // ---------------------------
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

    const clickElement = element => {
        ['mousedown', 'mouseup', 'click'].forEach(type => {
            element.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
        });
        console.log('Simulated click on', element);
    };

    const waitForHeaderBubbles = async (timeout = 3000) => {
        const startTime = Date.now();
        let bubbles = [];
        while (Date.now() - startTime < timeout) {
            bubbles = Array.from(document.querySelectorAll('div.docs-bubble.kix-header-footer-bubble'))
                .filter(bubble => window.getComputedStyle(bubble).display !== 'none');
            if (bubbles.length) break;
            await sleep(300);
        }
        return bubbles;
    };

    // New function to load header bubble using a chain of fallbacks.
    function clickHeaderChain() {
        // Attempt 1: Try to click the main Header option.
        const mainHeader = document.evaluate(
            "//div[contains(@class, 'goog-menuitem') and contains(@class, 'apps-menuitem') and not(contains(@class, 'goog-submenu'))]//span[contains(@aria-label, 'Header') and not(contains(@aria-label, 'Headers'))]",
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
        if (mainHeader) {
            clickElement(mainHeader.closest('div.goog-menuitem'));
            // Close any lingering menus.
            document.querySelectorAll('div.goog-menu').forEach(menu => menu.style.display = 'none');
            console.log("Main Header clicked immediately.");
            return;
        }

        // Attempt 2: Try the fallback "Headers & footers" option.
        const fallbackHeader = document.evaluate(
            "//div[contains(@class, 'goog-menuitem') and contains(@class, 'apps-menuitem') and contains(@class, 'goog-submenu')]//span[contains(@aria-label, 'Headers') and contains(@aria-label, 'footers')]",
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
        if (fallbackHeader) {
            clickElement(fallbackHeader.closest('div.goog-menuitem'));
            console.log("Fallback 'Headers & footers' clicked.");
            // Wait a bit then try for the main header.
            setTimeout(() => {
                const mainHeaderRetry = document.evaluate(
                    "//div[contains(@class, 'goog-menuitem') and contains(@class, 'apps-menuitem') and not(contains(@class, 'goog-submenu'))]//span[contains(@aria-label, 'Header') and not(contains(@aria-label, 'Headers'))]",
                    document,
                    null,
                    XPathResult.FIRST_ORDERED_NODE_TYPE,
                    null
                ).singleNodeValue;
                if (mainHeaderRetry) {
                    clickElement(mainHeaderRetry.closest('div.goog-menuitem'));
                    document.querySelectorAll('div.goog-menu').forEach(menu => menu.style.display = 'none');
                    console.log("Main Header clicked after fallback.");
                } else {
                    console.log("Main Header did not appear after clicking fallback.");
                }
            }, 500);
            return;
        }

        // Attempt 3: Use the Insert menu as a last resort.
        const insertMenu = document.querySelector('div#docs-insert-menu');
        if (insertMenu) {
            clickElement(insertMenu);
            console.log("Insert menu clicked as fallback.");
            setTimeout(() => {
                const fallbackHeaderRetry = document.evaluate(
                    "//div[contains(@class, 'goog-menuitem') and contains(@class, 'apps-menuitem') and contains(@class, 'goog-submenu')]//span[contains(@aria-label, 'Headers') and contains(@aria-label, 'footers')]",
                    document,
                    null,
                    XPathResult.FIRST_ORDERED_NODE_TYPE,
                    null
                ).singleNodeValue;
                if (fallbackHeaderRetry) {
                    clickElement(fallbackHeaderRetry.closest('div.goog-menuitem'));
                    console.log("'Headers & footers' clicked from Insert menu.");
                    setTimeout(() => {
                        const mainHeaderFinal = document.evaluate(
                            "//div[contains(@class, 'goog-menuitem') and contains(@class, 'apps-menuitem') and not(contains(@class, 'goog-submenu'))]//span[contains(@aria-label, 'Header') and not(contains(@aria-label, 'Headers'))]",
                            document,
                            null,
                            XPathResult.FIRST_ORDERED_NODE_TYPE,
                            null
                        ).singleNodeValue;
                        if (mainHeaderFinal) {
                            clickElement(mainHeaderFinal.closest('div.goog-menuitem'));
                            document.querySelectorAll('div.goog-menu').forEach(menu => menu.style.display = 'none');
                            console.log("Main Header clicked after Insert chain.");
                        } else {
                            console.log("Main Header did not appear after Insert chain.");
                        }
                    }, 500);
                } else {
                    console.log("'Headers & footers' did not appear after clicking Insert menu.");
                }
            }, 500);
        } else {
            console.log("Insert menu is missing.");
        }
    }

    const disableDocumentTabs = () => {
        const tabsElem = document.querySelector(
            'div.navigation-widget.navigation-widget-unified-styling.docs-material.navigation-widget-floating-navigation-button.navigation-location-indicator.outline-refresh.navigation-widget-hoverable.navigation-widget-chaptered.left-sidebar-container-content-child'
        );
        if (tabsElem) {
            tabsElem.style.pointerEvents = 'none';
            console.log("Document tabs disabled.");
        }
    };

    const enableDocumentTabs = () => {
        const tabsElem = document.querySelector(
            'div.navigation-widget.navigation-widget-unified-styling.docs-material.navigation-widget-floating-navigation-button.navigation-location-indicator.outline-refresh.navigation-widget-hoverable.navigation-widget-chaptered.left-sidebar-container-content-child'
        );
        if (tabsElem) {
            tabsElem.style.pointerEvents = '';
            console.log("Document tabs re-enabled.");
        }
    };


    // ---------------------------
    // 2. Global State & Constants
    // ---------------------------
    let currentTextDirection = 'ltr';
    let thumbnailOverlay = null;
    let overlayPositionObserver = null;
    let thumbnailZoomFactor = 1;
    const ZOOM_RATIO = 1.15, MIN_ZOOM = 0.5, MAX_ZOOM = 2.0;
    const capturedPages = new Set();
    let captureTimeoutId = null;
    let mutationObserver = null;
    let isGroupingEnabled = false; // For heading grouping
    let isSectionGroupingEnabled = false; // New flag for section grouping
    const capturedPageData = {}; // { pageNumber: { thumbEntry, headings: Set() } }
    let headingGroupsArr = [];
    let cancelScrollSequence = false;
    let isThumbnailViewActive = false;
    let pagesViewButton = null;
    let progressBarContainer = null;
    let progressBarInner = null;
    let tabSelectionIntervalId = null;
    let cancelHeadingClicks = false;
    let initialSelectedTab = null; // Store the tab element that was selected when opening overlay

    // ---------------------------
    // 3. Document & Scroll Utilities
    // ---------------------------
    const getDocumentId = () =>
    (window.location.href.match(/\/d\/([a-zA-Z0-9_-]+)/) || [])[1] || 'default';

    const getScrollableElement = () => document.querySelector('.kix-appview-editor');

    const saveScrollPosition = () => {
        const scrollable = getScrollableElement();
        if (scrollable) {
            const scrollData = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
            scrollData[getDocumentId()] = scrollable.scrollTop;
            localStorage.setItem('googleDocsScrollData', JSON.stringify(scrollData));
        }
    };

    const restoreScrollPosition = () => {
        const scrollable = getScrollableElement();
        const scrollData = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
        const pos = scrollData[getDocumentId()];
        if (scrollable && pos !== undefined) {
            scrollable.scrollTop = parseInt(pos, 10);
        }
    };

    // ---------------------------
    // 4. Thumbnail Overlay Management
    // ---------------------------
    const updateOverlayPosition = () => {
        let topOffset = '50px';
        let ruler = document.getElementById('kix-horizontal-ruler') ||
            document.querySelector('div#docs-chrome[aria-label="Menu bar"]');
        if (ruler) topOffset = `${ruler.getBoundingClientRect().bottom}px`;

        let leftOffset = '10px';
        const sidebar = document.querySelector('.left-sidebar-container');
        if (sidebar) leftOffset = `${sidebar.getBoundingClientRect().right}px`;

        if (thumbnailOverlay) {
            Object.assign(thumbnailOverlay.style, { top: topOffset, left: leftOffset });
        }
    };

    const startOverlayPositionObserver = () => {
        const targets = [];
        const sidebar = document.querySelector('.left-sidebar-container');
        if (sidebar) targets.push(sidebar);
        let ruler = document.getElementById('kix-horizontal-ruler') ||
            document.querySelector('div#docs-chrome[aria-label="Menu bar"]');
        if (ruler) targets.push(ruler);
        if (!targets.length) return;
        overlayPositionObserver = new MutationObserver(updateOverlayPosition);
        targets.forEach(target =>
                        overlayPositionObserver.observe(target, { attributes: true, attributeFilter: ['style', 'class'] })
                       );
    };

    const stopOverlayPositionObserver = () => {
        if (overlayPositionObserver) {
            overlayPositionObserver.disconnect();
            overlayPositionObserver = null;
        }
    };

    const createThumbnailOverlay = () => {
        thumbnailOverlay = document.createElement('div');
        thumbnailOverlay.id = 'thumbnailOverlay';
        updateOverlayPosition();
        Object.assign(thumbnailOverlay.style, {
            position: 'fixed',
            right: '0',
            bottom: '0',
            background: '#f9fbfd',
            zIndex: '10000',
            overflowY: 'auto',
            display: 'flex',
            flexWrap: 'wrap',
            padding: '10px',
            alignContent: 'flex-start'
        });
        document.body.appendChild(thumbnailOverlay);
        startOverlayPositionObserver();
    };

    const removeThumbnailOverlay = () => {
        if (thumbnailOverlay) {
            thumbnailOverlay.remove();
            thumbnailOverlay = null;
        }
        stopOverlayPositionObserver();
    };

    // ---------------------------
    // 5. Thumbnail Display & Zoom
    // ---------------------------
    const insertThumbnailInOrder = (thumbElement, pageNumber) => {
        if (!thumbnailOverlay) return;
        const thumbnails = Array.from(thumbnailOverlay.querySelectorAll('.thumbnail-entry'));
        const index = thumbnails.findIndex(el => parseInt(el.dataset.pageNumber, 10) > pageNumber);
        if (index >= 0) {
            thumbnailOverlay.insertBefore(thumbElement, thumbnails[index]);
        } else {
            thumbnailOverlay.appendChild(thumbElement);
        }
    };

    const updateThumbnailZoom = () => {
        if (!thumbnailOverlay) return;
        thumbnailOverlay.querySelectorAll('.thumbnail-entry img').forEach(img => {
            img.style.width = `${200 * thumbnailZoomFactor}px`;
        });
    };

    const handleCtrlZoom = event => {
 // Zoom in (multiplicative)
        if (['=', 'Add', 'NumpadAdd'].includes(event.key)) {
            event.preventDefault();
            event.stopImmediatePropagation();
            thumbnailZoomFactor = Math.min(thumbnailZoomFactor * ZOOM_RATIO, MAX_ZOOM);
          updateThumbnailZoom();
       }
        // Zoom out (multiplicative)
        else if (['-', 'Subtract', 'NumpadSubtract'].includes(event.key)) {
            event.preventDefault();
            event.stopImmediatePropagation();
            thumbnailZoomFactor = Math.max(thumbnailZoomFactor / ZOOM_RATIO, MIN_ZOOM);
            updateThumbnailZoom();
        }
    };

    const attachZoomListeners = () => {
        window.addEventListener('keydown', handleCtrlZoom, true);
        document.querySelectorAll('iframe').forEach(iframe => {
            try {
                (iframe.contentDocument || iframe.contentWindow.document)
                    .addEventListener('keydown', handleCtrlZoom, true);
            } catch (err) {
                console.error('Error attaching zoom listener:', err);
            }
        });
    };

    const detachZoomListeners = () => {
        window.removeEventListener('keydown', handleCtrlZoom, true);
        document.querySelectorAll('iframe').forEach(iframe => {
            try {
                (iframe.contentDocument || iframe.contentWindow.document)
                    .removeEventListener('keydown', handleCtrlZoom, true);
            } catch (err) {
                console.error('Error detaching zoom listener:', err);
            }
        });
    };

    // Update text direction and re-render thumbnails (flat or grouped)
    const setTextDirection = direction => {
        currentTextDirection = direction;
        if (thumbnailOverlay) {
            thumbnailOverlay.style.direction = direction;
            updateOverlayPosition();
            // When RTL, add extra padding on the right.
            if (direction === 'rtl') {
                thumbnailOverlay.style.padding = '10px 20px 10px 10px'; // top, right, bottom, left
            } else {
                thumbnailOverlay.style.padding = '10px';
            }
        }
        if (!isGroupingEnabled && !isSectionGroupingEnabled) {
            thumbnailOverlay.replaceChildren();
            Object.keys(capturedPageData)
                .map(Number)
                .sort((a, b) => a - b)
                .forEach(pageNum => {
                const data = capturedPageData[pageNum];
                if (data?.thumbEntry) {
                    thumbnailOverlay.appendChild(data.thumbEntry);
                }
            });
        } else if (isGroupingEnabled) {
            renderGroupedThumbnails();
        } else if (isSectionGroupingEnabled) {
            renderSectionGroupedPages();
        }
    };

    // ---------------------------
    // 6. Page Capture Module
    // ---------------------------
    const getCurrentPageNumber = () => {
        const tooltip = document.querySelector('div.jfk-tooltip-contentId[style*="direction: ltr"]');
        if (tooltip) {
            const match = tooltip.textContent.match(/(\d+)\s+of/);
            return match ? parseInt(match[1], 10) : null;
        }
        return null;
    };

    const getCurrentSelectedHeading = () => {
        const headingElem = document.querySelector(
            '#chapter-container-t\\.wf0m5iat3jku > div.chapter-item.chapter-item-subchapters-indent-enabled > div.updating-navigation-item-list > div.navigation-item-list.goog-container .navigation-item.location-indicator-highlight'
        );
        if (headingElem) {
            const content = headingElem.querySelector('.navigation-item-content');
            const headingText = content ? content.textContent.trim() : 'Unknown';
            const levelMatch = content ? content.className.match(/navigation-item-level-(\d+)/) : null;
            return { headingElem, headingText, navLevel: levelMatch ? parseInt(levelMatch[1], 10) : 0 };
        }
        return null;
    };

    const getHighestParentHeading = currentHeadingElem => {
        const parentContainer = currentHeadingElem.parentNode;
        const allHeadings = Array.from(parentContainer.querySelectorAll('.navigation-item'));
        const currentIndex = allHeadings.indexOf(currentHeadingElem);
        let highestHeading = currentHeadingElem, highestLevel = Infinity;
        for (let i = currentIndex - 1; i >= 0; i--) {
            const elem = allHeadings[i];
            const content = elem.querySelector('.navigation-item-content');
            if (content) {
                const level = (content.className.match(/navigation-item-level-(\d+)/) || [])[1] || 0;
                if (level < highestLevel) {
                    highestLevel = level;
                    highestHeading = elem;
                }
            }
        }
        const content = highestHeading.querySelector('.navigation-item-content');
        return {
            headingElem: highestHeading,
            headingText: content ? content.textContent.trim() : 'Unknown',
            navLevel: highestLevel === Infinity ? 0 : highestLevel
        };
    };

    // ---------------------------
    // 7. Grouping Functions (Heading Grouping)
    // ---------------------------
    const recordHeadingStartingPages = async () => {
        // Disable document tabs while heading clicks are active.
        disableDocumentTabs();

        // Reset grouping array and ensure heading clicks can run.
        headingGroupsArr = [];
        cancelHeadingClicks = false;
        const selectedTab = document.querySelector('.chapter-item-label-and-buttons-container-selected');
        if (!selectedTab) {
            console.warn("No selected tab found.");
            enableDocumentTabs();
            return;
        }
        const chapterItem = selectedTab.closest('.chapter-item');
        if (!chapterItem) {
            console.warn("Selected tab does not have a chapter-item parent.");
            enableDocumentTabs();
            return;
        }
        const navList = chapterItem.querySelector('.updating-navigation-item-list');
        if (!navList) {
            console.warn("No navigation item list found under the selected tab.");
            enableDocumentTabs();
            return;
        }

        const topLevelHeadings = Array.from(navList.querySelectorAll('.navigation-item')).filter(item => {
            const content = item.querySelector('.navigation-item-content');
            return content?.classList.contains('navigation-item-level-0');
        });
        const totalHeadings = topLevelHeadings.length;
        let clickedCount = 0;
        const progressBar = document.getElementById('headingProgressBar');
        if (progressBar) {
            progressBar.style.width = `0%`;
        }

        try {
            for (const item of topLevelHeadings) {
                if (cancelHeadingClicks) {
                    console.log("Heading click loop cancelled.");
                    break;
                }
                clickElement(item);
                clickedCount++;
                if (progressBar) {
                    progressBar.style.width = `${(clickedCount / totalHeadings) * 100}%`;
                }
                await sleep(100);
                const currentPage = getCurrentPageNumber();
                if (currentPage) {
                    const headingText = item.querySelector('.navigation-item-content')?.textContent.trim() || 'Unknown';
                    headingGroupsArr.push({ headingText, navLevel: 0, startingPage: currentPage, pages: new Set() });
                }
            }
        } finally {
            // Always re-enable document tabs after the loop completes or is interrupted.
            enableDocumentTabs();
        }
    };


    const assignPagesToGroups = () => {
        headingGroupsArr.sort((a, b) => a.startingPage - b.startingPage);
        const capturedPageNumbers = Object.keys(capturedPageData).map(Number).sort((a, b) => a - b);
        const groupsByStartingPage = {};
        headingGroupsArr.forEach(group => {
            groupsByStartingPage[group.startingPage] = groupsByStartingPage[group.startingPage] || [];
            groupsByStartingPage[group.startingPage].push(group);
        });
        const distinctStartPages = Object.keys(groupsByStartingPage).map(Number).sort((a, b) => a - b);
        distinctStartPages.forEach((sp, index) => {
            const nextSP = index < distinctStartPages.length - 1 ? distinctStartPages[index + 1] : Infinity;
            const pagesInRange = capturedPageNumbers.filter(pageNum => pageNum >= sp && pageNum < nextSP);
            const groups = groupsByStartingPage[sp];
            groups.forEach((group, idx) => {
                group.pages = (idx < groups.length - 1)
                    ? new Set(capturedPageNumbers.includes(sp) ? [sp] : [])
                : new Set(pagesInRange);
            });
        });
    };

    const capturePages = () => {
        if (!thumbnailOverlay) return;
        const pages = Array.from(document.querySelectorAll('.kix-page-paginated'));
        const scrollable = getScrollableElement();
        pages.forEach((page, index) => {
            const rotatingTileManager = page.closest('.kix-rotatingtilemanager.docs-ui-hit-region-surface');
            if (rotatingTileManager && rotatingTileManager.parentElement &&
                window.getComputedStyle(rotatingTileManager.parentElement).display === 'none') return;
            let pageNumber = parseInt(page.style.zIndex, 10);
            pageNumber = !isNaN(pageNumber) ? pageNumber + 1 : index + 1;
            if (capturedPages.has(pageNumber)) return;
            const canvas = page.querySelector('canvas.kix-canvas-tile-content');
            if (!canvas) return;
            // Force a reflow/repaint on the canvas.
            canvas.style.display = 'none';
            void canvas.offsetHeight;
            canvas.style.display = '';
            let dataUrl;
            try {
                dataUrl = canvas.toDataURL();
            } catch (err) {
                console.error('Error converting canvas to image:', err);
                return;
            }
            let pageScrollPos = 0;
            if (scrollable) {
                const containerRect = scrollable.getBoundingClientRect();
                const pageRect = page.getBoundingClientRect();
                pageScrollPos = scrollable.scrollTop + (pageRect.top - containerRect.top);
            }
            const thumbEntry = document.createElement('div');
            thumbEntry.className = 'thumbnail-entry';
            thumbEntry.dataset.pageNumber = pageNumber;
            thumbEntry.dataset.scrollPos = pageScrollPos;
            Object.assign(thumbEntry.style, {
                margin: '10px',
                textAlign: 'center',
                cursor: 'pointer',
                opacity: '0',
                transition: 'opacity 0.5s'
            });
            const img = document.createElement('img');
            img.src = dataUrl;
            img.style.width = `${200 * thumbnailZoomFactor}px`;
            img.style.height = 'auto';
            img.name = `page_${pageNumber}`;
            thumbEntry.appendChild(img);
            const pageLabel = document.createElement('div');
            pageLabel.innerText = `Page ${pageNumber}`;
            pageLabel.style.marginTop = '5px';
            thumbEntry.appendChild(pageLabel);
            thumbEntry.addEventListener('click', () => {
                exitThumbnailView();
                isGroupingEnabled = false;
                isSectionGroupingEnabled = false;
                const targetPos = parseInt(thumbEntry.dataset.scrollPos, 10);
                if (scrollable) scrollable.scrollTop = targetPos;
            });
            if (!isGroupingEnabled && !isSectionGroupingEnabled) {
                insertThumbnailInOrder(thumbEntry, pageNumber);
            }
            capturedPageData[pageNumber] = { thumbEntry, headings: new Set() };
            setTimeout(() => { thumbEntry.style.opacity = '1'; }, 50);
            capturedPages.add(pageNumber);
        });
        updateProgressBar();
    };

    const capturePagesWrapper = () => {
        capturePages();
        if (isGroupingEnabled) {
            renderGroupedThumbnails();
        } else if (isSectionGroupingEnabled) {
            // Call without waiting so that scrolling is not blocked.
            renderSectionGroupedPages();
        }
    };

    const startObservingPages = () => {
        const container = document.querySelector('.kix-rotatingtilemanager-content');
        if (!container) return;
        mutationObserver = new MutationObserver(() => {
            clearTimeout(captureTimeoutId);
            captureTimeoutId = setTimeout(capturePagesWrapper, 100);
        });
        mutationObserver.observe(container, { childList: true, subtree: true, attributes: true });
    };

    const stopObservingPages = () => {
        if (mutationObserver) {
            mutationObserver.disconnect();
            mutationObserver = null;
        }
    };

    // ---------------------------
    // 8. Fast Scroll Simulation
    // ---------------------------
    const simulateScrollSequence = async () => {
        const scrollable = getScrollableElement();
        if (!scrollable) return;
        scrollable.scrollTop = 0;
        startObservingPages();
        capturePagesWrapper();
        const intervalId = setInterval(() => {
            if (cancelScrollSequence) {
                clearInterval(intervalId);
                return;
            }
            const { scrollTop, clientHeight, scrollHeight } = scrollable;
            const newScroll = scrollTop + clientHeight;
            if (newScroll >= scrollHeight) {
                scrollable.scrollTop = scrollHeight;
                capturePagesWrapper();
                if (progressBarInner) progressBarInner.style.background = '#2684fc';
                clearInterval(intervalId);
                console.log("Reached bottom of page, scroll sequence complete.");
                recordHeadingStartingPages().then(() => {
                    assignPagesToGroups();
                    if (isGroupingEnabled) {
                        renderGroupedThumbnails();
                    }
                });
            } else {
                scrollable.scrollTop = newScroll;
                capturePagesWrapper();
            }
        }, 50);
    };

    // ---------------------------
    // 9. Thumbnail View Toggle & Cleanup
    // ---------------------------
    const createProgressBar = () => {
        if (!pagesViewButton) return;
        progressBarContainer = document.createElement('div');
        Object.assign(progressBarContainer.style, {
            position: 'absolute',
            top: '0',
            left: '50%',
            transform: 'translateX(-50%) translateY(1px)',
            width: '60%',
            height: '2px',
            background: 'transparent',
            pointerEvents: 'none'
        });
        progressBarInner = document.createElement('div');
        Object.assign(progressBarInner.style, {
            height: '100%',
            width: '0%',
            background: '#555'
        });
        progressBarContainer.appendChild(progressBarInner);
        pagesViewButton.appendChild(progressBarContainer);
    };

    const removeProgressBar = () => {
        progressBarContainer?.remove();
        progressBarContainer = progressBarInner = null;
    };

    const updateProgressBar = () => {
        if (!progressBarInner) return;
        const tooltipElem = document.querySelector('div.jfk-tooltip-contentId[style*="direction: ltr"]');
        if (!tooltipElem) return setTimeout(updateProgressBar, 500);
        const match = tooltipElem.textContent.match(/of\s*(\d+)/);
        if (!match) return;
        const maxPages = parseInt(match[1], 10);
        if (maxPages === 0) return;
        const progressPercent = Math.min((capturedPages.size / maxPages) * 100, 100);
        progressBarInner.style.width = progressPercent + '%';
    };

    const toggleThumbnailView = () => {
        if (!isThumbnailViewActive) {
            cancelScrollSequence = false;
            // Reset heading click cancellation.
            cancelHeadingClicks = false;
            // Store the initially selected tab element.
            initialSelectedTab = document.querySelector('.chapter-item-label-and-buttons-container.chapter-item-label-and-buttons-container-selected');

            // Start monitoring the selected tab.
            tabSelectionIntervalId = setInterval(() => {
                // If the initially selected tab no longer exists or lost the selected class, close overlay.
                if (!initialSelectedTab || !initialSelectedTab.classList.contains('chapter-item-label-and-buttons-container-selected')) {
                    console.log("Current tab is no longer selected. Closing overlay.");
                    exitThumbnailView();
                }
            }, 1000);

            saveScrollPosition();
            createThumbnailOverlay();
            simulateScrollSequence();
            isThumbnailViewActive = true;
            attachZoomListeners();
            createProgressBar();
            createCustomMenu(pagesViewButton);
            pagesViewButton.classList.add('active');
            // Check for header bubble at the start.
            const headerBubbleExists = document.querySelector('div.docs-bubble.kix-header-footer-bubble') &&
                  window.getComputedStyle(document.querySelector('div.docs-bubble.kix-header-footer-bubble')).display !== 'none';
            if (headerBubbleExists) {
                console.log("Header bubble detected on Pages View open; section grouping available.");
            } else {
                console.log("No header bubble detected on Pages View open; section grouping disabled.");
            }
        } else {
            exitThumbnailView();
        }
    };

    const exitThumbnailView = (skipRestore = false) => {
        if (tabSelectionIntervalId) {
            clearInterval(tabSelectionIntervalId);
            tabSelectionIntervalId = null;
        }
        cancelHeadingClicks = true;
        cancelScrollSequence = true;
        removeThumbnailOverlay();
        removeProgressBar();
        document.getElementById('customMenu')?.remove();
        if (!skipRestore) restoreScrollPosition();
        stopObservingPages();
        capturedPages.clear();
        Object.keys(capturedPageData).forEach(key => delete capturedPageData[key]);
        headingGroupsArr = [];
        isThumbnailViewActive = false;
        isGroupingEnabled = false;
        isSectionGroupingEnabled = false;
        detachZoomListeners();
        pagesViewButton?.classList.remove('active');
    };

    // ---------------------------
    // 10. Button Management
    // ---------------------------
    const waitForElement = (selector, timeout = 20000) =>
    new Promise((resolve, reject) => {
        const observer = new MutationObserver((_, obs) => {
            const el = document.querySelector(selector);
            if (el) {
                obs.disconnect();
                resolve(el);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        setTimeout(() => {
            observer.disconnect();
            reject(new Error(`Timeout waiting for element: ${selector}`));
        }, timeout);
    });

    const addPagesViewButton = referenceElement => {
        const newButton = document.createElement('div');
        newButton.setAttribute('role', 'button');
        newButton.className = 'goog-inline-block jfk-button jfk-button-standard kix-outlines-widget-header-add-chapter-button-icon custom-pages-view-button';
        newButton.tabIndex = 0;
        newButton.setAttribute('data-tooltip-class', 'kix-outlines-widget-header-add-chapter-button-tooltip');
        newButton.setAttribute('aria-label', 'Pages view');
        newButton.setAttribute('data-tooltip', 'Pages view');

        const iconWrapper = document.createElement('div');
        iconWrapper.className = 'docs-icon goog-inline-block';
        const iconInner = document.createElement('div');
        iconInner.className = 'docs-icon-img-container docs-icon-img docs-icon-editors-ia-content-copy';

        iconInner.setAttribute('aria-hidden', 'true');
        iconInner.textContent = '\u00A0';
        iconWrapper.appendChild(iconInner);
        newButton.appendChild(iconWrapper);

        // Inject button styles.
        const style = document.createElement('style');
        style.textContent = `
      .custom-pages-view-button {
        user-select: none;
        direction: ltr;
        visibility: visible;
        position: relative;
        display: inline-block;
        cursor: pointer;
        font-size: 11px;
        text-align: center;
        white-space: nowrap;
        line-height: 27px;
        outline: 0;
        color: #333;
        border: 1px solid rgba(0,0,0,.1);
        font-family: "Google Sans",Roboto,RobotoDraft,Helvetica,Arial,sans-serif;
        font-weight: 500;
        background-color: transparent;
        background-image: none;
        border-radius: 50%;
        border-width: 0;
        box-shadow: none;
        min-width: unset;
        height: 28px;
        margin: 2px;
        padding: 0;
        width: 28px;
        transition: background-color 0.3s ease;
      }
      .custom-pages-view-button:hover {
        background-color: rgba(0,0,0,0.1);
      }
      .custom-pages-view-button.active,
      .custom-menu-button.active {
        background-color: rgba(0,0,0,0.1);
      }
      .custom-menu-button:focus,
      .custom-menu-button:active {
        outline: none !important;
        border: none !important;
        box-shadow: none !important;
      }
      .custom-menu-button.active {
        outline: none !important;
        border: none !important;
        box-shadow: none !important;
        background-color: rgba(0,0,0,0.1);
      }
    `;
      document.head.appendChild(style);
      referenceElement.parentNode.insertBefore(newButton, referenceElement.nextSibling);
      pagesViewButton = newButton;
      newButton.addEventListener('click', toggleThumbnailView);
  };

    const createCustomMenu = referenceButton => {
        const menu = document.createElement('div');
        menu.id = 'customMenu';
        const rect = referenceButton.getBoundingClientRect();
        Object.assign(menu.style, {
            position: 'absolute',
            left: (rect.left - 180) + 'px',
            top: (rect.top - 55) + 'px',
            width: '200px',
            backgroundColor: '#fff',
            borderRadius: '26px',
            padding: '8px',
            zIndex: '10001',
            boxShadow: '0px 1px 4px rgba(0, 0, 0, 0.2)',
            fontFamily: 'Roboto,RobotoDraft,Helvetica,Arial,sans-serif',
            fontWeight: '400',
            fontSize: '13px',
            color: '#000',
            cursor: 'default',
            userSelect: 'none',
            display: 'flex',
            justifyContent: 'space-around',
            alignItems: 'center'
        });

        for (let i = 1; i <= 4; i++) {
            const btn = document.createElement('div');
            btn.className = 'goog-inline-block jfk-button jfk-button-standard custom-pages-view-button custom-menu-button';
            btn.tabIndex = 0;
            Object.assign(btn.style, {
                width: '28px',
                height: '28px',
                borderRadius: '50%',
                cursor: 'pointer',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
            });
            if (i === 1) {
                // Section grouping button
                btn.setAttribute('data-tooltip', 'Section grouping');
            } else if (i === 2) {
                // Heading grouping button
                btn.setAttribute('data-tooltip', 'Heading grouping');
            } else if (i === 3) {
                btn.setAttribute('data-tooltip', 'Left-to-right');
            } else if (i === 4) {
                btn.setAttribute('data-tooltip', 'Right-to-left');
            }
            btn.setAttribute('data-tooltip-class', 'kix-outlines-widget-header-add-chapter-button-tooltip');

            const iconWrapper = document.createElement('div');
            iconWrapper.className = 'docs-icon goog-inline-block goog-menuitem-icon';
            iconWrapper.setAttribute('aria-hidden', 'true');
            iconWrapper.style.userSelect = 'none';

            const iconInner = document.createElement('div');
            iconInner.className = 'docs-icon-img-container docs-icon-img';
            iconInner.style.userSelect = 'none';
            if (i === 1 || i === 2) {
  iconInner.style.transform = 'translate(1px, 0px)';
}
btn.appendChild(iconInner);


            if (i === 1) {
                // Section Grouping: square grid icon
                iconInner.classList.add('docs-icon-editors-ia-square-grid-view');
                btn.addEventListener('click', async () => {
                    isSectionGroupingEnabled = !isSectionGroupingEnabled;
                    btn.classList.toggle('active', isSectionGroupingEnabled);
                    if (isSectionGroupingEnabled) {
                        // First, check if header bubbles already exist.
                        let headerBubbles = Array.from(document.querySelectorAll('div.docs-bubble.kix-header-footer-bubble'))
                        .filter(bubble => window.getComputedStyle(bubble).display !== 'none');
                        if (!headerBubbles.length) {
                            // If not, trigger the header chain and wait until bubbles appear (up to 3000ms).
                            clickHeaderChain();
                            headerBubbles = await waitForHeaderBubbles(3000);
                        }
                        if (headerBubbles.length) {
                            renderSectionGroupedPages();
                        } else {
                            console.log("Header bubble not loaded; section grouping will not be applied.");
                        }
                    } else {
                        // Revert to flat view.
                        if (thumbnailOverlay) {
                            thumbnailOverlay.replaceChildren();
                            Object.keys(capturedPageData)
                                .map(Number)
                                .sort((a, b) => a - b)
                                .forEach(pageNum => {
                                const data = capturedPageData[pageNum];
                                if (data?.thumbEntry) {
                                    thumbnailOverlay.appendChild(data.thumbEntry);
                                }
                            });
                        }
                    }
                });
            }
            else if (i === 2) {
                // Heading Grouping: header/footer icon
                iconInner.classList.add('docs-icon-editors-ia-header-footer');
                // Ensure the button is positioned relative to its container so the progress bar aligns correctly.
                btn.style.position = 'relative';
                btn.addEventListener('click', () => {
                    isGroupingEnabled = !isGroupingEnabled;
                    btn.classList.toggle('active', isGroupingEnabled);
                    if (isGroupingEnabled) {
                        renderGroupedThumbnails();
                    } else {
                        if (thumbnailOverlay) {
                            thumbnailOverlay.replaceChildren();
                            Object.keys(capturedPageData)
                                .map(Number)
                                .sort((a, b) => a - b)
                                .forEach(pageNum => {
                                const data = capturedPageData[pageNum];
                                if (data?.thumbEntry) {
                                    thumbnailOverlay.appendChild(data.thumbEntry);
                                }
                            });
                        }
                    }
                });
                // Create a progress bar container similar to the pages view progress bar.
                const headingProgressContainer = document.createElement('div');
                Object.assign(headingProgressContainer.style, {
                    position: 'absolute',
                    top: '0',
                    left: '50%',
                    transform: 'translateX(-50%) translateY(1px)',
                    width: '60%',
                    height: '2px',
                    background: 'transparent',
                    pointerEvents: 'none'
                });
                const headingProgressInner = document.createElement('div');
                headingProgressInner.id = 'headingProgressBar';
                Object.assign(headingProgressInner.style, {
                    height: '100%',
                    width: '0%',
                    background: '#2684fc'
                });
                headingProgressContainer.appendChild(headingProgressInner);
                btn.appendChild(headingProgressContainer);
            }

            else if (i === 3) {
                iconInner.classList.add('docs-icon-text-ltr-20');
                btn.addEventListener('click', () => {
                    setTextDirection('ltr');
                    document.querySelectorAll('#customMenu .custom-menu-button[data-tooltip="Left-to-right"], #customMenu .custom-menu-button[data-tooltip="Right-to-left"]')
                        .forEach(btnElem => {
                        btnElem.classList.toggle('active', btnElem.getAttribute('data-tooltip') === 'Left-to-right');
                    });
                });
                if (currentTextDirection === 'ltr') btn.classList.add('active');
            } else if (i === 4) {
                iconInner.classList.add('docs-icon-text-rtl-20');
                btn.addEventListener('click', () => {
                    setTextDirection('rtl');
                    document.querySelectorAll('#customMenu .custom-menu-button[data-tooltip="Left-to-right"], #customMenu .custom-menu-button[data-tooltip="Right-to-left"]')
                        .forEach(btnElem => {
                        btnElem.classList.toggle('active', btnElem.getAttribute('data-tooltip') === 'Right-to-left');
                    });
                });
                if (currentTextDirection === 'rtl') btn.classList.add('active');
            }
            iconWrapper.appendChild(iconInner);
            btn.appendChild(iconWrapper);
            menu.appendChild(btn);
        }
        document.body.appendChild(menu);
        return menu;
    };

    // ---------------------------
    // 11. Grouped Thumbnails Rendering (Heading Grouping)
    // ---------------------------
    const renderGroupedThumbnails = () => {
        if (!thumbnailOverlay) return;
        thumbnailOverlay.replaceChildren();
        headingGroupsArr.sort((a, b) => a.startingPage - b.startingPage).forEach(group => {
            const groupContainer = document.createElement('div');
            Object.assign(groupContainer.style, {
                margin: '10px',
                padding: '10px',
                border: '1px solid #ccc',
                borderRadius: '8px',
                background: '#fff'
            });
            const headingTitle = document.createElement('div');
            headingTitle.textContent = group.headingText;
            headingTitle.style.fontWeight = 'bold';
            headingTitle.style.marginBottom = '5px';
            groupContainer.appendChild(headingTitle);
            const thumbsContainer = document.createElement('div');
            Object.assign(thumbsContainer.style, {
                display: 'flex',
                flexWrap: 'wrap',
                gap: '8px'
            });
            Array.from(group.pages).sort((a, b) => a - b).forEach(pageNum => {
                const data = capturedPageData[pageNum];
                if (data?.thumbEntry) {
                    const thumbClone = data.thumbEntry.cloneNode(true);
                    thumbClone.addEventListener('click', () => {
                        exitThumbnailView();
                        const targetPos = parseInt(thumbClone.dataset.scrollPos, 10);
                        const scrollable = getScrollableElement();
                        if (scrollable) scrollable.scrollTop = targetPos;
                    });
                    thumbsContainer.appendChild(thumbClone);
                }
            });
            groupContainer.appendChild(thumbsContainer);
            thumbnailOverlay.appendChild(groupContainer);
        });
    };

    // ---------------------------
    // 11a. Section Grouping: Render Pages Grouped by Section
    // ---------------------------
    // Helper function to close unwanted menus (mimics the Borders and Shading behavior)
    function closeMenusExcept(exceptionsSelectors = []) {
        const allMenus = document.querySelectorAll(
            'div.goog-menu.goog-menu-vertical.docs-material.shell-menu.goog-menu-noaccel, div.goog-menu.goog-menu-vertical.docs-material'
        );
        allMenus.forEach(menu => {
            if (!exceptionsSelectors.some(selector => menu.matches(selector))) {
                menu.style.display = 'none';
                console.log('Closed unwanted menu:', menu);
            }
        });
        // Additionally, click the document body to clear any overlays.
        document.body.click();
    }

    // Asynchronous helper to ensure header bubbles are loaded
    const ensureHeaderBubblesLoaded = async (pauseScroll = true) => {
        // Check for existing visible header/footer bubbles.
        let headerBubbles = Array.from(document.querySelectorAll('div.docs-bubble.kix-header-footer-bubble'))
        .filter(bubble => window.getComputedStyle(bubble).display !== 'none');
        if (headerBubbles.length) {
            return headerBubbles;
        }

        // If no bubbles are found and pauseScroll is allowed, pause scroll simulation.
        if (pauseScroll) {
            cancelScrollSequence = true;
        }

        // --- Attempt 1: Direct click of "Header" ---
        let headerLabel = document.querySelector('div.goog-menuitem[role="menuitem"] span.goog-menuitem-label[aria-label^="Header"]');
        if (headerLabel) {
            const headerMenuItem = headerLabel.closest('div.goog-menuitem[role="menuitem"]');
            if (headerMenuItem) {
                console.log("Clicking Header menu item.");
                clickElement(headerMenuItem);
                await sleep(300);
                headerBubbles = await waitForHeaderBubbles();
                if (headerBubbles.length) {
                    closeMenusExcept([]); // Remove any menus that remain.
                    return headerBubbles;
                }
            }
        }

        // --- Attempt 2: Click "Header & Footer" then "Header" ---
        let headerFooterLabel = document.querySelector('div.goog-menuitem[role="menuitem"] span.goog-menuitem-label[aria-label*="Header & Footer"]');
        if (headerFooterLabel) {
            const headerFooterMenuItem = headerFooterLabel.closest('div.goog-menuitem[role="menuitem"]');
            if (headerFooterMenuItem) {
                console.log("Clicking Header & Footer menu item.");
                clickElement(headerFooterMenuItem);
                await sleep(300);
                headerLabel = document.querySelector('div.goog-menuitem[role="menuitem"] span.goog-menuitem-label[aria-label^="Header"]');
                if (headerLabel) {
                    const headerMenuItem = headerLabel.closest('div.goog-menuitem[role="menuitem"]');
                    if (headerMenuItem) {
                        console.log("Clicking Header menu item after Header & Footer.");
                        clickElement(headerMenuItem);
                        await sleep(300);
                    }
                }
                headerBubbles = await waitForHeaderBubbles();
                if (headerBubbles.length) {
                    closeMenusExcept([]);
                    return headerBubbles;
                }
            }
        }

        // --- Attempt 3: Use the Insert menu to insert header/footer ---
        const insertMenuButton = document.getElementById('docs-insert-menu');
        if (insertMenuButton) {
            console.log("Clicking Insert menu button to try to insert header and footer.");
            clickElement(insertMenuButton);
            await sleep(300);
            let insertHeaderFooterLabel = document.querySelector('div.goog-menuitem span.goog-menuitem-label[aria-label*="Header & footer"]');
            if (insertHeaderFooterLabel) {
                const insertHeaderFooterMenuItem = insertHeaderFooterLabel.closest('div.goog-menuitem[role="menuitem"]');
                if (insertHeaderFooterMenuItem) {
                    clickElement(insertHeaderFooterMenuItem);
                    await sleep(300);
                    headerLabel = document.querySelector('div.goog-menuitem[role="menuitem"] span.goog-menuitem-label[aria-label^="Header"]');
                    if (headerLabel) {
                        const headerMenuItem = headerLabel.closest('div.goog-menuitem[role="menuitem"]');
                        if (headerMenuItem) {
                            console.log("Clicking Header menu item after Insert menu.");
                            clickElement(headerMenuItem);
                            await sleep(300);
                        }
                    }
                }
                headerBubbles = await waitForHeaderBubbles();
                if (headerBubbles.length) {
                    closeMenusExcept([]);
                    return headerBubbles;
                }
            }
        }

        console.log("Header/footer bubble elements still not found after all attempts.");
        return headerBubbles;
    };

    // Updated renderSectionGroupedPages function that uses the helper above
    const renderSectionGroupedPages = async () => {
        if (!thumbnailOverlay) return;

        // Ensure header bubbles are loaded (only if they aren't already)
        const headerBubbles = await ensureHeaderBubblesLoaded();
        if (!headerBubbles.length) {
            console.log("Cannot proceed with section grouping; header bubbles are unavailable.");
            return;
        }

        // Group pages by section using header bubble descriptions.
        const sectionGroups = {};
        headerBubbles.forEach((bubble, index) => {
            const pageNumber = index + 1;
            const descriptionSpan = bubble.querySelector('.kix-header-footer-bubble-description');
            if (!descriptionSpan) {
                console.log("Header/footer bubble description not found for page " + pageNumber);
                return;
            }
            const text = descriptionSpan.textContent;
            // Capture strings like "Section 1" from descriptions such as "Header - Section 1"
            const match = text.match(/Section\s+\S+/);
            const sectionKey = match ? match[0] : 'Unknown Section';
            if (!sectionGroups[sectionKey]) sectionGroups[sectionKey] = new Set();
            sectionGroups[sectionKey].add(pageNumber);
        });

        // Clear the overlay and build the grouped thumbnails.
        thumbnailOverlay.replaceChildren();
        Object.keys(sectionGroups).sort().forEach(sectionKey => {
            const groupContainer = document.createElement('div');
            Object.assign(groupContainer.style, {
                margin: '10px',
                padding: '10px',
                border: '1px solid #ccc',
                borderRadius: '8px',
                background: '#fff'
            });
            const header = document.createElement('div');
            header.textContent = sectionKey;
            header.style.fontWeight = 'bold';
            header.style.marginBottom = '5px';
            groupContainer.appendChild(header);
            const thumbsContainer = document.createElement('div');
            Object.assign(thumbsContainer.style, {
                display: 'flex',
                flexWrap: 'wrap',
                gap: '8px'
            });
            Array.from(sectionGroups[sectionKey]).sort((a, b) => a - b).forEach(pageNum => {
                const data = capturedPageData[pageNum];
                if (data?.thumbEntry) {
                    const thumbClone = data.thumbEntry.cloneNode(true);
                    thumbClone.addEventListener('click', () => {
                        exitThumbnailView();
                        const targetPos = parseInt(thumbClone.dataset.scrollPos, 10);
                        const scrollable = getScrollableElement();
                        if (scrollable) scrollable.scrollTop = targetPos;
                    });
                    thumbsContainer.appendChild(thumbClone);
                }
            });
            groupContainer.appendChild(thumbsContainer);
            thumbnailOverlay.appendChild(groupContainer);
        });
    };

    // ---------------------------
    // 12. Initialization
    // ---------------------------
    waitForElement('.kix-outlines-widget-header-add-chapter-button')
        .then(addPagesViewButton)
        .catch(console.error);
})();