Gmail Single Email View

Shows one email at a time in Gmail inbox, with enhanced email detection and accurate counter for empty inbox.

当前为 2025-06-19 提交的版本,查看 最新版本

// ==UserScript==
// @name         Gmail Single Email View
// @namespace    http://tampermonkey.net/
// @version      2.0 // Incremented version to signify major bug fix for counter
// @description  Shows one email at a time in Gmail inbox, with enhanced email detection and accurate counter for empty inbox.
// @author       Your Name
// @match        https://mail.google.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let emailRows = [];
    let currentIndex = 0;
    let navControls;
    let prevBtn, nextBtn, countDisplay;
    let observer = null;
    let updateTimeoutId = null;
    let isPluginInitialized = false; // Flag to ensure initialization runs only once

    /**
     * Injects necessary CSS into the Gmail page to ensure elements can be hidden and essential UI remains visible.
     */
    function injectCss() {
        const styleId = 'single-email-view-style';
        if (document.getElementById(styleId)) {
            return; // Style already injected
        }

        const style = document.createElement('style');
        style.id = styleId;
        style.textContent = `
            /* Styling for the plugin's navigation buttons */
            #single-email-nav-controls {
                position: fixed;
                bottom: 20px;
                left: 50%;
                transform: translateX(-50%);
                z-index: 10000; /* Ensure it's above Gmail's UI */
                display: flex;
                gap: 10px;
                background-color: #fff;
                padding: 10px 20px;
                border-radius: 12px; /* Rounded corners */
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* Soft shadow */
                border: 1px solid #ddd;
            }

            #single-email-nav-controls button {
                padding: 10px 20px;
                font-size: 16px;
                cursor: pointer;
                border: none;
                border-radius: 8px; /* Rounded corners for buttons */
                background-color: #4285f4; /* Gmail blue */
                color: white;
                transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }

            #single-email-nav-controls button:hover {
                background-color: #357ae8; /* Darker blue on hover */
                transform: translateY(-1px);
            }

            #single-email-nav-controls button:active {
                background-color: #2a6ac3;
                transform: translateY(0);
                box-shadow: none;
            }

            #single-email-nav-controls button:disabled {
                background-color: #cccccc;
                cursor: not-allowed;
                box-shadow: none;
            }

            /* Styling for the current email count display */
            #email-count-display {
                display: flex;
                align-items: center;
                justify-content: center;
                min-width: 80px;
                padding: 0 10px;
                font-weight: bold;
                color: #333;
            }

            /* IMPORTANT: Only hide email rows with this class to avoid hiding everything */
            .single-email-hidden {
                display: none !important;
            }

            /* Ensure essential Gmail UI components (like sidebars, toolbars) remain visible */
            .nH.nn.aKz { /* Pagination/toolbar at the bottom */
                display: flex !important; /* This should generally be flex */
            }
            .G-atb, .Cp { /* Top toolbar and category tabs */
                display: block !important; /* These are usually block or flex */
            }

            /* Explicitly ensure left navigation sidebar is visible (but let internal layout be managed by Gmail) */
            div[role="navigation"] { /* The main navigation sidebar container */
                display: block !important;
            }
        `;
        document.head.appendChild(style);
        console.log('Gmail Single Email View: Injected CSS styles.');
    }

    /**
     * Initializes the Gmail Single Email View plugin.
     * Creates navigation controls using createElement to avoid TrustedHTML issues.
     */
    function initGmailSingleEmailView() {
        if (isPluginInitialized) {
            console.log('Gmail Single Email View: Plugin already initialized, skipping.');
            return;
        }

        console.log('Gmail Single Email View: Initializing plugin interface...');
        injectCss(); // Ensure CSS is injected first

        navControls = document.getElementById('single-email-nav-controls');
        if (!navControls) {
            navControls = document.createElement('div');
            navControls.id = 'single-email-nav-controls';
            document.body.appendChild(navControls);
            console.log('Gmail Single Email View: Navigation controls container created and appended.');

            // Create buttons and display element separately and append them
            prevBtn = document.createElement('button');
            prevBtn.id = 'prevEmailBtn';
            prevBtn.textContent = 'Previous';
            prevBtn.disabled = true; // Start disabled
            navControls.appendChild(prevBtn);

            countDisplay = document.createElement('div');
            countDisplay.id = 'email-count-display';
            navControls.appendChild(countDisplay);

            nextBtn = document.createElement('button');
            nextBtn.id = 'nextEmailBtn';
            nextBtn.textContent = 'Next';
            nextBtn.disabled = true; // Start disabled
            navControls.appendChild(nextBtn);

            console.log('Gmail Single Email View: Navigation buttons and display element created and appended.');

        } else {
            console.log('Gmail Single Email View: Navigation controls already exist.');
            // If controls exist, just get references
            prevBtn = document.getElementById('prevEmailBtn');
            nextBtn = document.getElementById('nextEmailBtn');
            countDisplay = document.getElementById('email-count-display');
        }

        addEventListeners();
        updateEmailList();
        isPluginInitialized = true; // Set flag after successful initialization
    }

    /**
     * Adds event listeners to the navigation buttons.
     */
    function addEventListeners() {
        if (prevBtn && !prevBtn.hasAttribute('data-listener-added')) {
            prevBtn.onclick = showPreviousEmail;
            prevBtn.setAttribute('data-listener-added', 'true');
            console.log('Gmail Single Email View: Previous button event listener added.');
        }
        if (nextBtn && !nextBtn.hasAttribute('data-listener-added')) {
            nextBtn.onclick = showNextEmail;
            nextBtn.setAttribute('data-listener-added', 'true');
            console.log('Gmail Single Email View: Next button event listener added.');
        }
    }

    /**
     * Resets the plugin state when no emails are found.
     * This ensures the counter correctly displays "No Emails Found".
     */
    function resetPluginState() {
        emailRows = []; // Clear the list of emails
        currentIndex = 0; // Reset index
        console.log('Gmail Single Email View: Resetting plugin state: no emails found.');
        showEmail(0); // This will ensure all hidden emails are revealed, and internal display logic runs
        updateButtonStates();
        updateCountDisplay();
    }

    /**
     * Updates the list of email rows and displays the current email.
     */
    function updateEmailList() {
        console.log('Gmail Single Email View: Scanning for email rows...');
        let potentialEmailRows = [];

        // Check if the "No new emails!" message is present
        const noEmailsMessage = document.querySelector('div[role="main"] .nH.nZ.nn');
        if (noEmailsMessage && noEmailsMessage.textContent.includes('No new emails!')) {
            console.log('Gmail Single Email View: "No new emails!" message detected.');
            resetPluginState(); // Call the dedicated reset function
            return; // Exit early as there are no emails to process
        }

        // Primary selector based on your provided HTML structure
        potentialEmailRows = Array.from(document.querySelectorAll('tr.zA.yO[role="row"]:has([role="checkbox"])'));
        console.log(`Gmail Single Email View: Found ${potentialEmailRows.length} potential email rows with primary selector (tr.zA.yO[role="row"]:has([role="checkbox"])).`);

        // If primary selector finds few or none, try other common patterns in Gmail
        if (potentialEmailRows.length === 0 || potentialEmailRows.length < 5) {
            console.log('Gmail Single Email View: Primary selector found few/no rows, trying alternative selectors...');
            // Fallback to more general selectors if the primary one isn't fruitful
            let altRows = Array.from(document.querySelectorAll('div[role="main"] tr[role="row"], .AO > .xY, [aria-labelledby^="thread_"], [aria-labelledby^="msg_"]'));
            // Only use altRows if they find more results
            if (altRows.length > potentialEmailRows.length) {
                potentialEmailRows = altRows;
                console.log(`Gmail Single Email View: Found ${potentialEmailRows.length} rows with alternative combined selectors.`);
            }
        }

        // Filter out non-email rows (e.g., header, pagination, ads, empty message rows, non-email UI elements)
        emailRows = potentialEmailRows.filter(row => {
            // Check for elements characteristic of an actual email row:
            const hasCheckbox = row.querySelector('[role="checkbox"]');
            const hasGridcell = row.querySelector('[role="gridcell"]');
            const isColumnHeader = row.querySelector('[role="columnheader"]');

            // More robust subject/sender checks based on your HTML
            const hasSubject = row.querySelector('.bog, .ts, .y6');
            const hasSender = row.querySelector('.yP, .zF, .go');

            // The row should look like email content AND not be a header.
            // Require at least a checkbox, a gridcell, and EITHER a subject/sender.
            const isEmailContentRow = hasCheckbox && hasGridcell && (hasSubject || hasSender) && !isColumnHeader;

            // Explicitly exclude known non-email rows or UI elements that might get caught by broad selectors
            const isKnownNonEmailUI = row.classList.contains('Cp') ||
                                     row.classList.contains('nH') ||
                                     row.classList.contains('G-atb') ||
                                     row.classList.contains('Bu') ||
                                     row.querySelector('.aeJ') ||
                                     row.matches('.ads, .AP');

            if (!isEmailContentRow || isKnownNonEmailUI) {
                // console.log('Gmail Single Email View: Filtering out non-email row (lacks essential email characteristics or is known UI element):', row.outerHTML);
            }
            return isEmailContentRow && !isKnownNonEmailUI;
        });

        console.log(`Gmail Single Email View: Filtered down to ${emailRows.length} actual email rows.`);

        // After filtering, if no emails are found, reset the plugin state
        if (emailRows.length === 0) {
            console.log('Gmail Single Email View: Filtered result is 0 emails. Calling resetPluginState.');
            resetPluginState();
        } else {
            // If emails are found, proceed with normal display logic
            // Adjust currentIndex if it's out of bounds after filtering
            if (currentIndex >= emailRows.length) {
                currentIndex = emailRows.length - 1;
            } else if (currentIndex < 0) {
                currentIndex = 0;
            }
            showEmail(currentIndex);
            updateButtonStates();
            updateCountDisplay();
        }
    }

    /**
     * Shows only the email at the given index and hides all others.
     * @param {number} index - The index of the email to show.
     */
    function showEmail(index) {
        // Always ensure ALL previously hidden emails are made visible before re-hiding
        document.querySelectorAll('.single-email-hidden').forEach(el => el.classList.remove('single-email-hidden'));

        if (emailRows.length === 0) {
            console.log('No email rows found to display, ensuring all elements are visible and buttons disabled.');
            updateButtonStates(); // Ensure buttons are disabled
            updateCountDisplay(); // Ensure counter says "No Emails Found"
            return; // No emails to show, so nothing to hide
        }

        if (index < 0 || index >= emailRows.length) {
            console.warn(`Attempted to show email at invalid index: ${index}. Clamping to valid range.`);
            index = Math.max(0, Math.min(index, emailRows.length - 1));
        }

        emailRows.forEach((row, i) => {
            if (i === index) {
                row.classList.remove('single-email-hidden');
                row.style.display = ''; // Ensure display is not explicitly set to none by previous operation
            } else {
                row.classList.add('single-email-hidden');
            }
        });
        currentIndex = index;
        updateButtonStates();
        updateCountDisplay();
        console.log(`Showing email ${currentIndex + 1} of ${emailRows.length}.`);
    }

    /**
     * Shows the next email in the list.
     */
    function showNextEmail() {
        if (currentIndex < emailRows.length - 1) {
            console.log('Moving to next email...');
            showEmail(currentIndex + 1);
        } else {
            console.log('Already at the last email.');
        }
    }

    /**
     * Shows the previous email in the list.
     */
    function showPreviousEmail() {
        if (currentIndex > 0) {
            console.log('Moving to previous email...');
            showEmail(currentIndex - 1);
        } else {
            console.log('Already at the first email.');
        }
    }

    /**
     * Updates the disabled state of the navigation buttons.
     */
    function updateButtonStates() {
        if (prevBtn) {
            prevBtn.disabled = currentIndex === 0 || emailRows.length === 0;
        }
        if (nextBtn) {
            nextBtn.disabled = currentIndex >= emailRows.length - 1 || emailRows.length === 0;
        }
    }

    /**
     * Updates the display showing current email index and total count.
     */
    function updateCountDisplay() {
        if (countDisplay) {
            countDisplay.textContent = emailRows.length > 0 ? `${currentIndex + 1} / ${emailRows.length}` : 'No Emails Found';
        }
    }

    /**
     * Observes the DOM for changes in Gmail.
     * This function now observes the entire document body for stability.
     */
    function setupMutationObserver() {
        // Disconnect existing observer if it exists to prevent duplicates
        if (observer) {
            observer.disconnect();
            console.log('Gmail Single Email View: Disconnected existing observer.');
        }
        isPluginInitialized = false; // Reset initialization flag on new observer setup

        // Observe the entire document body for maximum robustness
        observer = new MutationObserver((mutations) => {
            // Check for relevant changes that would impact email list
            const relevantChange = mutations.some(mutation =>
                mutation.addedNodes.length > 0 && Array.from(mutation.addedNodes).some(node =>
                    node.nodeType === Node.ELEMENT_NODE &&
                    (node.matches('tr.zA.yO[role="row"]') || node.matches('.nH.nZ.nn')) // Check for new email rows or "No new emails!" message
                ) ||
                mutation.removedNodes.length > 0 && Array.from(mutation.removedNodes).some(node =>
                    node.nodeType === Node.ELEMENT_NODE &&
                    (node.matches('tr.zA.yO[role="row"]') || node.matches('.nH.nZ.nn'))
                ) ||
                // Also trigger if the immediate parent of email rows (the tbody or table) changes its children
                (mutation.type === 'childList' && (mutation.target.matches('tbody') || mutation.target.matches('table.F.cf.zt')))
            );


            // Only re-scan if a relevant change occurred
            if (relevantChange) {
                clearTimeout(updateTimeoutId);
                updateTimeoutId = setTimeout(() => {
                    console.log('Gmail Single Email View: DOM changes detected, re-scanning emails...');
                    initGmailSingleEmailView(); // Re-initialize to ensure state and elements are correct
                }, 500);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false });
        console.log('Gmail Single Email View: Started observing document.body for changes.');
        // Initial call to initGmailSingleEmailView
        initGmailSingleEmailView();
    }

    // Initial setup attempts
    window.addEventListener('load', setupMutationObserver);
    document.addEventListener('DOMContentLoaded', setupMutationObserver);

    // Fallback for immediate execution if DOM is already ready
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setupMutationObserver();
    }

})();