Gmail Single Email View

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==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();
    }

})();