// ==UserScript== // @name Gmail Premium UI Suite // @namespace https://github.com/SysAdminDoc/MailPro-Enhancement-Suite // @version 6.9 // @description The ultimate Gmail revamp. Features a dynamic JS-based chat collapse, "nuclear" reply header removal, plus advanced signature hiding and UI tools. // @author Matthew Parker // @match https://mail.google.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @downloadURL https://raw.githubusercontent.com/SysAdminDoc/MailPro-Enhancement-Suite/main/Gmail%20Premium%20UI%20Suite.user.js // @updateURL https://raw.githubusercontent.com/SysAdminDoc/MailPro-Enhancement-Suite/main/Gmail%20Premium%20UI%20Suite.meta.js // @run-at document-start // ==/UserScript== (function() { 'use strict'; // —————————————————————————————————————————————————————————————————————————— // ~ V6.9 UPDATES ~ // // 1. Refined Dark Mode Pane: // - Added new styles to the dark email pane for icon and background // consistency (e.g., white reply/star icons). // // —————————————————————————————————————————————————————————————————————————— // ————————————————————— // 0. DYNAMIC CONTENT HIDING ENGINE // ————————————————————— let dynamicHidingObserver = null; const activeHidingRules = new Map(); const observerCallback = (mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // Process only element nodes for (const rule of activeHidingRules.values()) { try { rule(node); } catch (e) { console.error('[Gmail Premium] Error applying hiding rule:', e); } } } }); } } }; function startObserver() { if (dynamicHidingObserver) return; dynamicHidingObserver = new MutationObserver(observerCallback); dynamicHidingObserver.observe(document.body, { childList: true, subtree: true }); } function stopObserver() { if (dynamicHidingObserver) { dynamicHidingObserver.disconnect(); dynamicHidingObserver = null; } } function addHidingRule(id, ruleFn) { activeHidingRules.set(id, ruleFn); if (activeHidingRules.size === 1) { startObserver(); } ruleFn(document.body); } function removeHidingRule(id) { activeHidingRules.delete(id); if (activeHidingRules.size === 0) { stopObserver(); } // General cleanup for elements hidden by this rule document.querySelectorAll(`[data-gm-hidden-by="${id}"]`).forEach(el => { el.style.display = ''; el.removeAttribute('data-gm-hidden-by'); }); } // ————————————————————— // 1. SETTINGS MANAGER // ————————————————————— const settingsManager = { defaults: { // UI & Visuals settingsButton: true, glowingStarred: true, squarifyTheme: false, animatedCompose: true, animatedStars: true, styleDateTime: true, // Themes gmailDarkMode: false, gmailDarkModePane: true, // Layout customCSS: true, styleReplyButton: true, composeRecipientBorder: true, collapseChatSidebar: true, hideChatWithHoverLip: false, // Productivity showEmail: true, fmtToolbar: true, uiTweaks: true, contactChipDoubleClick: true, // Header Elements hideAppsGrid: false, hideProfileBox: false, hideTopBarSupport: false, hideTopBarSettings: false, // Hubspot hubspotActivityIndicator: false, hideHubspotControls: false, hideHubspotLogTracker: false, hideHubspotProfileButton: false, hideHubspotContactIcon: false, // AI & Tools hideGeminiHelpMeWrite: false, hideAskGemini: false, hideLoomButton: true, hideSummarizeEmail: true, hideSmartFeaturesBanner: true, // Declutter hideOrgWarnings: true, hideMiscClutter: false, hideEmailLabels: false, hideLabelsSection: false, hideDiscoverMore: false, hideProfilePicture: false, hideEverythingElseHeader: false, hideStarredHeader: false, hideSubjectToolbar: false, // Email Thread Declutter nukeReplyMetadata: true, nukeReplyMetadataSimple: false, nukeReplyMetadataShowCc: false, nukeReplyMetadataShowBcc: false, nukeReplyMetadataRemovePleasantries: true, flatReplyChain: false, hideReactionButton: true, hideAllSignaturesInChain: true, }, async load() { let savedSettings = await GM_getValue('gmPremiumSettings', {}); // Clean up old settings from previous versions const oldKeys = ['hideReplyHeaders', 'hideSignaturesInReplies', 'hideDeviceSignatures', 'hideOutlookMobileSignature', 'hideReplyForwardMetadata', 'hideExternalSignatures', 'hideMySignatureInChains', 'mySignatureKeywords', 'themeToggle', 'darkLoadingScreen']; oldKeys.forEach(key => { if (savedSettings.hasOwnProperty(key)) { delete savedSettings[key]; } }); return { ...this.defaults, ...savedSettings }; }, async save(settings) { await GM_setValue('gmPremiumSettings', settings); }, async getFirstRunStatus() { return await GM_getValue('hasRunBefore', false); }, async setFirstRunStatus(hasRun) { await GM_setValue('hasRunBefore', hasRun); } }; // ————————————————————— // 2. FEATURE DEFINITIONS // ————————————————————— const features = [ // Group: UI & Visuals { id: 'settingsButton', name: 'Floating Settings Button', description: 'Shows a floating gear icon to open the settings panel.', group: 'UI & Visuals', _element: null, init() { const btn = document.createElement('button'); btn.id = 'gm-floating-settings-btn'; btn.title = 'Open Gmail Premium Settings'; btn.appendChild(createCogSvg()); btn.onclick = () => document.body.classList.toggle('gm-panel-open'); document.body.appendChild(btn); this._element = btn; }, destroy() { this._element?.remove(); } }, { id: 'glowingStarred', name: 'Glowing Starred Section', description: 'Adds a subtle glow effect to the "Starred" email section in your inbox.', group: 'UI & Visuals', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-glowing-starred'; this._styleElement.textContent = ` div.ae4:has(span.qh[title="Starred"]), div.ae4:has(span.qh:contains("Starred")) { box-shadow: 0 0 12px 2px rgba(250, 215, 60, 0.7) !important; border: 1px solid rgba(250, 215, 60, 0.8) !important; border-radius: 8px !important; margin-bottom: 10px !important; } div.ae4:has(span.qh[title="Starred"]) .F.cf.zt, div.ae4:has(span.qh:contains("Starred")) .F.cf.zt { border-radius: 8px; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, { id: 'squarifyTheme', name: 'Squarify UI Elements', description: 'Removes rounded corners from all elements for a sharp, squared-off look.', group: 'UI & Visuals', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-squarify-theme'; this._styleElement.textContent = ` *, *::before, *::after { border-radius: 0 !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'animatedCompose', name: 'Animated Compose Button', description: 'Applies a lively, breathing animation to the Compose button.', group: 'UI & Visuals', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-animated-compose'; this._styleElement.textContent = ` .aic > .z0 > .T-I.T-I-KE { background: linear-gradient(135deg, #a8e063, #56ab2f) !important; color: white !important; font-weight: bold !important; text-shadow: 1px 1px 2px rgba(0,0,0,0.25); border: 1px solid rgba(255, 255, 255, 0.2) !important; border-radius: 0 !important; width: 220px !important; position: relative; overflow: hidden; animation: gm-breathing-pulse 4s ease-in-out infinite !important; transition: all 0.3s ease !important; } .aic > .z0 > .T-I.T-I-KE::before { content: ''; position: absolute; top: 0; left: -85%; width: 60%; height: 100%; background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.25) 50%, rgba(255,255,255,0) 100%); transform: skewX(-25deg); } .aic > .z0 > .T-I.T-I-KE:hover::before { animation: gm-sheen 1s ease-out 1; } @keyframes gm-breathing-pulse { 0% { transform: scale(1); opacity: 0.95; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } 50% { transform: scale(1.02); opacity: 1; box-shadow: 0 4px 15px rgba(86, 171, 47, 0.4); } 100% { transform: scale(1); opacity: 0.95; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } } @keyframes gm-sheen { from { left: -85%; } to { left: 125%; } } .Cr.ada { padding-left: 16px !important; } .aic .T-I-KE .T-I-J3 { justify-content: center !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'animatedStars', name: 'Animated Star Icons', description: 'Replaces the static star icons with a glowing, animated version.', group: 'UI & Visuals', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-animated-stars'; this._styleElement.textContent = ` .T-KT-Jp, .zd[aria-checked="true"] .T-KT { background-image: url('data:image/svg+xml,') !important; background-position: center !important; background-repeat: no-repeat !important; animation: gm-star-glow 1.5s infinite alternate !important; } .T-KT-Jp img, .zd[aria-checked="true"] .T-KT img { opacity: 0 !important; } @keyframes gm-star-glow { from { filter: drop-shadow(0 0 2px #ffc107); } to { filter: drop-shadow(0 0 6px #ffeb3b); } } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'styleDateTime', name: 'Style Email Date/Time', description: 'Applies custom colors and background to the date/time stamp in emails.', group: 'UI & Visuals', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-style-datetime'; this._styleElement.textContent = ` span.g3 { color: #999999 !important; background-color: #d0e0e3 !important; font-weight: 700 !important; font-style: normal !important; padding: 2px 4px; border-radius: 3px; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, // Group: Themes { id: 'gmailDarkMode', name: 'Enable Gmail Dark Mode', description: "Toggles Gmail's native dark theme and applies a dark loading screen to prevent white flash.", group: 'Themes', _styleElement: null, _loadingStyleElement: null, _paneStyleElement: null, preInit() { // This part runs immediately at document-start to prevent the white flash this._loadingStyleElement = document.createElement('style'); this._loadingStyleElement.id = 'gm-dark-loading-screen'; this._loadingStyleElement.textContent = ` /* full-page dark underlay to prevent white flash */ html, body { background-color: #121212 !important; } /* kill every CSS animation/transition */ *, *::before, *::after { animation: none !important; transition: none !important; } /* override Gmail’s loader */ #loading, #stb, .la-i > div, .la-k .la-m, .la-i > .la-m, .la-k .la-l, .la-k .la-r { background-color: #121212 !important; border: none !important; } /* loader text/links */ .msg, .msgb, .submit_as_link, #loading a { color: #e0e0e0 !important; } `; (document.head || document.documentElement).appendChild(this._loadingStyleElement); }, init() { // This part runs after the main Gmail UI is stable if (this._styleElement) return; this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-native-dark-theme'; // Fetch and apply Gmail's native dark theme const CSS_URL = 'https://mail.google.com/_/scs/mail-static/_/ss/k=gmail.main.C6cicS7fXaU.L.W.O/am=qAEDHAAA-MDlf76A_0qMPQAADPjX-f7VB_7Mb3huMgSy4CECRgQSBegDIhMFfyLy4XWs2-Ay7OMPCQAIQLsjm_04SIQtNH7UOoROacJlGAEAAAAAAAAAAAAAAAAAAAAeHgIC/d=1/excm=at/rs=AHGWq9DdJea2KTVAzJaogOueR9p3iW7rWQ'; GM_xmlhttpRequest({ method: 'GET', url: CSS_URL, onload: (response) => { if (response.status === 200 && this._styleElement) { this._styleElement.textContent = response.responseText; } else { console.error('[Gmail Premium] Failed to load dark mode CSS.'); showToast('Failed to load dark mode CSS.', true); } }, onerror: () => { console.error('[Gmail Premium] Error fetching dark mode CSS.'); showToast('Error fetching dark mode CSS.', true); } }); // Add dark chat roster styles this._styleElement.textContent += ` /* parent Gmail frame: roster + container */ #talk_roster, .VK.s.ik, .aay { background-color: #121212 !important; color: #e0e0e0 !important; } /* borders */ #talk_roster, .VK.s.ik { border-color: #333 !important; } /* iframe container */ #gtn-roster-iframe-id { background-color: #121212 !important; border: none !important; } `; document.head.appendChild(this._styleElement); // Add dark email pane if enabled if (appState.settings.gmailDarkModePane) { this._paneStyleElement = document.createElement('style'); this._paneStyleElement.id = 'gm-dark-email-view'; this._paneStyleElement.textContent = ` .HM .ahe::before { border: 0px !important; } /* compose button */ .J-M, [role="navigation"] [role="button"] { background-color: #333 !important; box-shadow: none !important; border: 1px solid #222 !important; color: #ddd !important; } /* dropdowns */ .bAp.b8.UC .vh, .ajA, .nH .Hy .m, .J-N-JT, .J-JK-JT, .J-LC-JT, form[role="search"], form[role="search"] table, form[role="search"] div, form[role="search"] td { background-color: #1c1c1c !important; } .az9, .agP, .aoU, .az4, .aoI, .aoT, .Hp, .nH .Hy .m, .J-N-Jz, .J-JK-Jz, [role="menuitemcheckbox"], form[role="search"], form[role="search"] table, form[role="search"] div, form[role="search"] td { color: #ddd !important; } .ZF-z6, .ZF-zT, .ZF-Av .lJ, .ZF-Av .lN, .aaZ, .aoT, .la-k .la-m, #loading, .HM .ahe::after, .aDg > .aDj, .azX, .nr, .iY, .IG, .GQ, .Ap, .aoP .Ar, .Am, .aDg > .aDj, .J-Z, .aC3, .aC2, .aDg > .aDj, .HM .ahe::before, .agP, .agh, .bbV, .aGb, .wO { background-color: #222 !important; } .HM .I5 { border: 1px solid #444 !important; } .afC, .nr, [aria-label="Search mail"], .amn > .ams, .az9 .aDp, .IG .Iy .az9 { color: #999 !important; } .az9, .im { color: #666 !important; } /* text */ .bs1 + .bs3, .btj + .aD, .ado b, .hx, .hx .gD, .hx .hb, .ac2, .IG, .Am, .ha > .hP, .gt, .gt div, .gt p, .gt h1, .gt h2, .gt h3, .gt h4, .gt h5, .gt h6, .gt figcaption, .gt td, .gt span, .gt font, .msgb, .J-M, .J-N, .agd .J-M-JJ input { color: #ddd !important; } .agd .J-M-JJ input, .la-i div, [role="navigation"] [role="button"], .aYy, .gt figcaption { background-color: #1c1c1c !important; border-radius: 4px; } .gsib_a.gb_jf.aJh, form[role="search"] tr:hover td, form[role="search"] tr:hover td div, .gt div:not(.aYy):not([role="button"]):not([role="menu"]):not([role="menuitemcheckbox"]):not([role="menuitem"]), .gt td, .gt span, .gt table, .gt h1, .gt h2, .gt h3, .gt h4, .gt h5, .gt h6, .gt table tbody tr td, .gt a, .gt p, .gt ul, .gt li, .gt center, [bgcolor] *, .gstt tbody tr td table tbody tr:hover, form[role="search"] div .gstt tbody tr:hover { background-color: transparent !important; } .afC, .afA, .agJ, .adp, .h9, .adI, .aHn { background-color: #1c1c1c !important; } .btb, .zA:hover, .afC { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .3),0 4px 8px 3px rgba(0, 0, 0, .15); } .hG.T-I-atl, .hG.T-I-atl.T-I-JW, .hG.T-I-atl:focus { border-color: #000 !important; } /* message links */ .Ol.Nk, .msgb input, .adI .B9, .h8, .gt a { color: #8bc4ff !important; } /* toolbar */ .Hl, .Hq, .Ha, [role="menuitemcheckbox"] > div > div, [role="listbox"] .J-Z-M-I-J6-H > .J-Z-M-I-JG, div.ajR .ajT, .btC .dv, .btC .aaA.a1, .btC .J-N-JX.a1, .btC .aaA.e5, .aaZ .J-N-JX.e5, .btC .aaA.QT, .btC .J-N-JX.QT, .btC .aaA.aA7, .aaZ .J-N-JX.aA7, .btC .aaA.buc, .btC .J-N-JX.buc, .btC .aaA.BP, .aaZ .J-N-JX.BP, .btC .aaA.a5, .btC .aaA.a2X, .aaZ .J-N-JX.a5, .aaZ .J-N-JX.a2X, [role="toolbar"] [role="button"]:not(.H2):not(.Ol) { filter: invert(1) !important; } tr.aRp:hover { background-color: #333 !important; } div.nH.ao9.id.UG { background-color: #222222; } .x7, form[role="search"] .gssb_m tr:hover, .agJ:hover { background-color: #141313 !important; } .J-J5-Ji.btA [role="button"] { background-color: #111 !important; } /* spam button and notice */ .bzx, .bzr:hover { background-color: #222; color: #cccccd; } /* Overall Background */ .iY, .iY .Bu { background: transparent !important } /* Star */ .bi4 > .T-KT:not(.T-KT-Jp):not(.byM)::before { background-image: url("https://www.gstatic.com/images/icons/material/system/1x/star_border_white_20dp.png") !important } /* Reply */ .hB, /* Inline */ .mL /* In ... menu */ { background-image: url("https://www.gstatic.com/images/icons/material/system/1x/reply_white_20dp.png") !important } /* Reply All */ .mK { background-image: url("https://www.gstatic.com/images/icons/material/system/1x/reply_all_white_20dp.png") !important } /* Forward (In ... menu) */ .mI { background-image: url("https://www.gstatic.com/images/icons/material/system/1x/forward_white_20dp.png") !important } `; document.head.appendChild(this._paneStyleElement); } }, destroy() { this._styleElement?.remove(); this._styleElement = null; this._loadingStyleElement?.remove(); this._loadingStyleElement = null; this._paneStyleElement?.remove(); this._paneStyleElement = null; }, }, // Group: Layout { id: 'customCSS', name: 'Core Layout Fixes', description: 'Applies essential CSS tweaks for a cleaner, full-width layout.', group: 'Layout', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-custom-css'; this._styleElement.textContent = ` div[role="main"] { padding-top: 10px; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, { id: 'styleReplyButton', name: 'Style Reply Button', description: 'Applies custom borders and padding to the main reply button.', group: 'Layout', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-style-reply-button'; this._styleElement.textContent = ` div.T-I.J-J5-Ji.T-I-Js-IF.bsQ.T-I-ax7.L3 { border-style: solid !important; border-color: #d9d9d9 !important; height: 25px !important; padding: 0 !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, { id: 'composeRecipientBorder', name: 'Compose Recipient Border', description: 'Adds a border below the To/Cc/Subject fields in the compose window.', group: 'Layout', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-compose-recipient-border'; this._styleElement.textContent = ` div.et { border-color: #efefef !important; border-style: solid !important; border-width: 0 0 1px 0 !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, { id: 'collapseChatSidebar', name: 'Collapse Chat Sidebar', description: 'Shrinks the chat/navigation sidebar to a minimal icon-only bar.', group: 'Layout', _observer: null, init() { const COLLAPSED_WIDTH = 56; // px function updateCollapse() { const mailLink = document.querySelector('div[role="link"][aria-label^="Mail"]'); const mainPanel = document.querySelector('div[role="main"]'); if (!mailLink || !mainPanel) return; const navPanel = mailLink.closest('div[role="navigation"], aside'); if (navPanel) { navPanel.style.width = `${COLLAPSED_WIDTH}px`; navPanel.style.minWidth = `${COLLAPSED_WIDTH}px`; } } updateCollapse(); this._observer = new MutationObserver(updateCollapse); this._observer.observe(document.body, { childList: true, subtree: true }); }, destroy() { this._observer?.disconnect(); this._observer = null; const mailLink = document.querySelector('div[role="link"][aria-label^="Mail"]'); if(!mailLink) return; const navPanel = mailLink.closest('div[role="navigation"], aside'); if (navPanel) { navPanel.style.width = ''; navPanel.style.minWidth = ''; } } }, { id: 'hideChatWithHoverLip', name: 'Hover to Reveal Chat', description: 'Hides the chat/nav panel completely, revealing it on hover over a left-side "lip".', group: 'Layout', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-chat-hover'; this._styleElement.textContent = ` .aeN.WR.a6o { position: fixed !important; left: 0; top: 64px; /* Adjust based on header height */ height: calc(100vh - 64px) !important; z-index: 1001; transform: translateX(calc(-100% + 5px)); transition: transform 0.25s ease-in-out; border-right: 1px solid #d3d3d3; } .aeN.WR.a6o::after { content: ''; position: absolute; right: 0; top: 0; width: 5px; height: 100%; background: #5e97f6; cursor: pointer; opacity: 0.7; transition: opacity 0.2s; } .aeN.WR.a6o:hover { transform: translateX(0); box-shadow: 2px 0 10px rgba(0,0,0,0.2); } .aeN.WR.a6o:hover::after { opacity: 1; } /* Adjust main content padding to account for hidden bar */ .bkK>.nH.oy8Mbf, .brC-aT5-aOt-I.brC-aT5-aOt-I { padding-left: 15px !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, // Group: Productivity { id: 'showEmail', name: 'Show Raw Emails', description: 'Displays the full email address instead of just the sender\'s name.', group: 'Productivity', _observer: null, init() { const showRawAddress = () => { document.querySelectorAll('span[email]').forEach(el => { const email = el.getAttribute('email'); if (email && el.textContent !== email) { el.textContent = email; } }); }; this._observer = new MutationObserver(showRawAddress); this._observer.observe(document.body, { childList: true, subtree: true }); showRawAddress(); }, destroy() { this._observer?.disconnect(); }, }, { id: 'fmtToolbar', name: 'Toggleable Formatting Bar', description: 'Adds a "Formatting" button to hide/show the compose toolbar.', group: 'Productivity', _observer: null, init() { const enhanceComposeWindow = (root) => { if (root.dataset.gmFmt) return; const toolbar = root.querySelector('.aX'); if (!toolbar) return; root.dataset.gmFmt = '1'; const btn = document.createElement('button'); btn.textContent = 'Formatting'; btn.className = 'gm-btn-secondary'; toolbar.style.visibility = 'hidden'; btn.onclick = () => { const isHidden = toolbar.style.visibility === 'hidden'; toolbar.style.visibility = isHidden ? 'visible' : 'hidden'; btn.classList.toggle('active', isHidden); }; toolbar.parentNode.insertBefore(btn, toolbar); }; this._observer = new MutationObserver(muts => { muts.forEach(m => m.addedNodes.forEach(n => { if (n.nodeType === 1 && n.matches('.AD')) enhanceComposeWindow(n); })); }); this._observer.observe(document.body, { childList: true }); document.querySelectorAll('.AD').forEach(enhanceComposeWindow); }, destroy() { this._observer?.disconnect(); document.querySelectorAll('.gm-btn-secondary').forEach(btn => btn.remove()); document.querySelectorAll('.AD[data-gm-fmt]').forEach(el => { const toolbar = el.querySelector('.aX'); if (toolbar) toolbar.style.visibility = 'visible'; delete el.dataset.gmFmt; }); }, }, { id: 'uiTweaks', name: 'Disable Compose Hover-Cards', description: 'Disables the distracting hover-card on email addresses in compose fields.', group: 'Productivity', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-uiTweaks'; this._styleElement.textContent = ` .agh .afV > .afW { pointer-events: none !important; } .agh .af6 { pointer-events: auto !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, { id: 'contactChipDoubleClick', name: 'Double-Click to Copy Email', description: 'Double-click a contact in To/Cc/Bcc to copy their email address.', group: 'Productivity', _clickHandler: null, init() { this._clickHandler = (e) => { const chip = e.target.closest('.afV[data-hovercard-id]'); if (!chip) return; const email = chip.getAttribute('data-hovercard-id'); if (!email) return; navigator.clipboard.writeText(email).then(() => { showToast('Email copied to clipboard!'); }).catch(err => { console.error('Failed to copy email: ', err); showToast('Failed to copy email.', true); }); }; document.body.addEventListener('dblclick', this._clickHandler); }, destroy() { if (this._clickHandler) { document.body.removeEventListener('dblclick', this._clickHandler); } } }, // Group: Header Elements { id: 'hideAppsGrid', name: 'Hide Google Apps Grid', description: 'Hides the 9-dot Google Apps grid menu in the main header.', group: 'Header Elements', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-apps-grid'; this._styleElement.textContent = `div.gb_Vc[aria-label="Google apps"] { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideProfileBox', name: 'Hide Account Profile Box', description: 'Hides your Google Account profile picture/avatar in the main header.', group: 'Header Elements', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-profile-box'; this._styleElement.textContent = `div.gb_Wa { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideTopBarSupport', name: 'Hide Top Bar Support Icon', description: 'Hides the question mark "Support" icon in the main header.', group: 'Header Elements', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-top-support'; this._styleElement.textContent = `.zo[data-tooltip="Support"] { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideTopBarSettings', name: 'Hide Top Bar Settings Icon', description: 'Hides the main Gmail "Settings" gear icon in the header.', group: 'Header Elements', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-top-settings'; this._styleElement.textContent = `.FI[data-tooltip="Settings"] { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, // Group: Hubspot { id: 'hubspotActivityIndicator', name: 'Hubspot Activity Indicator', description: 'Shows a colored bar at the top of the page indicating Hubspot status (Green/Red).', group: 'Hubspot', _elements: [], _observers: [], init() { const lip = document.createElement('div'); lip.id = 'gm-header-lip'; Object.assign(lip.style, { position: 'fixed', top: '0', left: '0', right: '0', height: '6px', zIndex: '10000', transition: 'background .3s ease', }); document.body.appendChild(lip); this._elements.push(lip); const colorLip = () => { const img = document.querySelector('img.kratos__button_img'); lip.style.background = img?.src.includes('sprocket-ok') ? '#2ecc71' : img?.src.includes('sprocket-off') ? '#e74c3c' : '#95a5a6'; }; const hubspotObserver = new MutationObserver(colorLip); hubspotObserver.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['src'] }); this._observers.push(hubspotObserver); colorLip(); }, destroy() { this._elements.forEach(el => el.remove()); this._observers.forEach(obs => obs.disconnect()); }, }, { id: 'hideHubspotControls', name: 'Hide Hubspot Compose Toolbar', description: 'Hides the Hubspot toolbar (Templates, Meetings, etc.) in the compose window.', group: 'Hubspot', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-hubspot-controls'; this._styleElement.textContent = `div#tool-row { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideHubspotLogTracker', name: 'Hide Log Tracker Indicator', description: 'Hides the Hubspot log/track status indicator that appears above the compose window.', group: 'Hubspot', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-hubspot-log-tracker'; this._styleElement.textContent = `.hubspot-logtrack-indicator { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideHubspotProfileButton', name: 'Hide Sender Profile Button', description: 'Hides the "View [Sender]\'s Profile" button from the Hubspot sidebar.', group: 'Hubspot', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-hubspot-profile-btn'; this._styleElement.textContent = `span.hubspot[data-add-or-view-contact-button-container] { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideHubspotContactIcon', name: 'Hide Hubspot Icon on Contact Chips', description: 'Hides the Hubspot sprocket icon on contact chips in the To/Cc/Bcc fields.', group: 'Hubspot', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-hubspot-contact-icon'; this._styleElement.textContent = `.agh .hubspot.indicator-container { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, // Group: AI & Tools { id: 'hideGeminiHelpMeWrite', name: 'Hide Gemini "Help me write"', description: 'Hides the "Help me write" AI prompt in the compose window.', group: 'AI & Tools', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-gemini-help-me-write'; this._styleElement.textContent = ` div.QT9oZc, div[data-promo-id="promo-mako-c2c"] { display: none !important; } span:has(i18n-string[data-key="compose.ComposeActionBar.generateTemplate.button"]) { display: none !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideAskGemini', name: 'Hide "Ask Gemini" Button', description: 'Hides the "Ask Gemini" button in the main top header.', group: 'AI & Tools', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-ask-gemini'; this._styleElement.textContent = `.e5IPTd.Zmxtcf { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideSummarizeEmail', name: 'Hide "Summarize Email" Button', description: 'Hides the Gemini "Summarize this email" button that appears at the top of an email thread.', group: 'AI & Tools', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-summarize-email'; this._styleElement.textContent = ` .einvLd { display: none !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideSmartFeaturesBanner', name: 'Hide "Smart Features" Banner', description: 'Hides the top banner prompting to "Turn on smart features".', group: 'AI & Tools', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-smart-features-banner'; // The class .ahS seems to be the primary container for this banner. this._styleElement.textContent = `.ahS { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideLoomButton', name: 'Hide Loom Button', description: 'Hides the Loom recording button in the compose window toolbar.', group: 'AI & Tools', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-loom-button'; this._styleElement.textContent = `.loom-button-td { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, // Group: Declutter { id: 'hideOrgWarnings', name: 'Hide "Outside Org" Warning', description: 'Hides the yellow banner warning about external recipients.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-org-warnings'; this._styleElement.textContent = `.ac4:has(.aeM) { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideEmailLabels', name: 'Hide Labels in Emails', description: 'Hides the label tags (e.g., "Inbox") shown at the top of an email.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-email-labels'; this._styleElement.textContent = `span[jsname="SjW3R"] { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideProfilePicture', name: 'Hide Profile Picture in Emails', description: 'Hides the sender\'s profile picture in the email view to save space.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-profile-picture'; this._styleElement.textContent = `td.aoY { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideEverythingElseHeader', name: 'Hide "Everything else" Header', description: 'Hides the header bar for the "Everything else" email group.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-everything-else-header'; this._styleElement.textContent = `div.ae4:has(div.aDa:not([title])) .aAr.Wg { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideLabelsSection', name: 'Hide "Labels" Section', description: 'Hides the entire "Labels" section in the left sidebar.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-labels-section'; this._styleElement.textContent = `div.ajl:has(h2.aWk:contains("Labels")), .aAw.FgKVne { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideStarredHeader', name: 'Hide "Starred" Section Header', description: 'Hides the header for the "Starred" email group.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-starred-header'; this._styleElement.textContent = `div.ae4:has(span.qh:contains("Starred")) > .Wg.aAD.aAr { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideSubjectToolbar', name: 'Hide Empty Subject Toolbar', description: 'Hides the empty space where the Hubspot toolbar was.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-subject-toolbar'; this._styleElement.textContent = `.SubjectToolbar.az6.aoD { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideDiscoverMore', name: 'Hide "Discover More" Button', description: 'Hides the "Discover more" button that can appear in the sidebar.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-discover-more'; this._styleElement.textContent = `.E0E5jb { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideMiscClutter', name: 'Hide Misc. Clutter', description: 'Hides various other elements like meeting ads and bottom banners.', group: 'Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-misc-clutter'; const selectors = ['.aT', '.aV2', '.bq9', '.Bs.nH.iY', '.aLO', '.apO', '.G-atb']; this._styleElement.textContent = `${selectors.join(',\n')} { display: none !important; }`; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, // GROUP: Email Thread Declutter { id: 'nukeReplyMetadata', name: 'Nuke Reply Metadata', group: 'Email Thread Declutter', description: 'Aggressively finds and replaces all reply/forward headers with a divider.', init() { const featId = this.id; const gmailDividerSelector = 'hr[style*="display:inline-block"][style*="width:98%"]'; // --- Pleasantry Remover Logic --- const TAIL_LINES = 5; const PLEASANTRIES = [ /^(thank you|thanks|many thanks|thanks in advance)[!.,]?$/i, /^(best|best regards|best wishes|warm regards|kind regards|warmly)[!.,]?$/i, /^(sincerely|sincerely yours|yours truly|yours sincerely|yours faithfully)[!.,]?$/i, /^(all the best)[!.,]?$/i, /^(cheers)[!.,]?$/i, /^(take care)[!.,]?$/i, /^(have a (?:great|nice) day)[!.,]?$/i, /^(enjoy)[!.,]?$/i, /^(appreciate (?:your )?help)[!.,]?$/i, /^(looking forward to hearing from you)[!.,]?$/i, /^(with gratitude)[!.,]?$/i, /^(respectfully)[!.,]?$/i ]; function stripPleasantries(el) { if (el.dataset.gmPleasantriesProcessed) return; el.dataset.gmPleasantriesProcessed = 'true'; const parts = el.innerHTML.split(/(|<\/div>|\r?\n)/gi); const lines = []; parts.forEach((chunk, idx) => { const text = chunk.replace(/<[^>]+>/g, '').trim(); if (text) lines.push({ text, idx }); }); const tail = lines.slice(-TAIL_LINES); let cutAt = parts.length; for (let { text, idx } of tail.reverse()) { // check from the bottom up if (PLEASANTRIES.some(rx => rx.test(text))) { cutAt = idx; const nextLine = lines.find(l => l.idx > idx); if (nextLine && /^[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*$/.test(nextLine.text)) { // also cut the name } break; } } if (cutAt < parts.length) { el.innerHTML = parts.slice(0, cutAt).join(''); } } // --- End Pleasantry Remover --- const processMetadataBlock = div => { // ... (existing metadata logic is unchanged) if (div.dataset.gmProcessed) return; div.dataset.gmProcessed = 'true'; const text = div.textContent .split('\n') .map(line => line.trim()) .filter(line => line) .join(' '); // Parse From const fromMatch = text.match(/From:\s*(.*?)\s*<([^>]+)>/i); let name = '', email = ''; if (fromMatch) { name = fromMatch[1].trim(); email = fromMatch[2].trim(); } // Parse Cc const ccMatch = text.match(/Cc:\s*(.*?)\s*<([^>]+)>/i); let ccName = '', ccEmail = ''; if (ccMatch) { ccName = ccMatch[1].trim(); ccEmail = ccMatch[2].trim(); } // Parse Bcc const bccMatch = text.match(/Bcc:\s*(.*?)\s*<([^>]+)>/i); let bccName = '', bccEmail = ''; if (bccMatch) { bccName = bccMatch[1].trim(); bccEmail = bccMatch[2].trim(); } // Hide full metadata div.style.display = 'none'; div.dataset.gmHiddenBy = featId; // Insert custom dashed divider const hr = document.createElement('hr'); hr.style.cssText = 'border:none; border-top:2px dashed #5e97f6; margin:12px 0'; hr.dataset.gmHiddenBy = `${featId}_hr`; div.parentNode.insertBefore(hr, div); // Inject simple lines in order: From, Cc, Bcc let last = hr; if (appState.settings.nukeReplyMetadataSimple && email) { const simpleFrom = document.createElement('div'); simpleFrom.dataset.gmHiddenBy = `${featId}_simple`; simpleFrom.style.cssText = 'margin:4px 0 12px; font-size:12px; color:#999'; simpleFrom.innerHTML = `From: ${name || email}`; last.insertAdjacentElement('afterend', simpleFrom); last = simpleFrom; } if (appState.settings.nukeReplyMetadataShowCc && ccEmail) { const simpleCc = document.createElement('div'); simpleCc.dataset.gmHiddenBy = `${featId}_cc`; simpleCc.style.cssText = 'margin:4px 0 12px; font-size:12px; color:#999'; simpleCc.innerHTML = `Cc: ${ccName || ccEmail}`; last.insertAdjacentElement('afterend', simpleCc); last = simpleCc; } if (appState.settings.nukeReplyMetadataShowBcc && bccEmail) { const simpleBcc = document.createElement('div'); simpleBcc.dataset.gmHiddenBy = `${featId}_bcc`; simpleBcc.style.cssText = 'margin:4px 0 12px; font-size:12px; color:#999'; simpleBcc.innerHTML = `Bcc: ${bccName || bccEmail}`; last.insertAdjacentElement('afterend', simpleBcc); } }; const findAndProcessMetadata = rootNode => { if (!rootNode.querySelectorAll) return; // RULE 0: Remove Gmail’s default divider rootNode.querySelectorAll(gmailDividerSelector).forEach(el => el.remove()); // Find and process all other metadata blocks const selectors = [ 'blockquote div[id*="divRplyFwdMsg"]', 'div.gmail_quote', 'div[style*="1pt solid"]', 'p', 'blockquote *' ]; rootNode.querySelectorAll(selectors.join(',')).forEach(el => { const txt = el.textContent.trim(); if ( el.matches('blockquote div[id*="divRplyFwdMsg"], div.gmail_quote') || /^-+ Forwarded message -+$/i.test(txt) || /^On\s.+\swrote:$/i.test(txt) || (el.tagName === 'P' && txt.includes('From:') && txt.includes('Sent:') && txt.includes('To:') && txt.includes('Subject:')) || (el.matches('div[style*="1pt solid"]') && txt.includes('From:')) ) { processMetadataBlock(el); } }); // NEW: Run pleasantry remover if enabled if (appState.settings.nukeReplyMetadataRemovePleasantries) { rootNode.querySelectorAll('.ii.gt, .a3s').forEach(stripPleasantries); } }; addHidingRule(featId, findAndProcessMetadata); }, destroy() { document.querySelectorAll(`[data-gm-hidden-by="${this.id}_simple"]`).forEach(el => el.remove()); document.querySelectorAll(`[data-gm-hidden-by="${this.id}_cc"]`).forEach(el => el.remove()); document.querySelectorAll(`[data-gm-hidden-by="${this.id}_bcc"]`).forEach(el => el.remove()); document.querySelectorAll(`[data-gm-hidden-by="${this.id}_hr"]`).forEach(el => el.remove()); removeHidingRule(this.id); document.querySelectorAll('[data-gm-processed], [data-gm-pleasentries-processed]').forEach(el => { el.removeAttribute('data-gm-processed'); el.removeAttribute('data-gm-pleasentries-processed'); }); // Note: Can't easily restore pleasantries, destroy() will just stop future removals. }, }, { id: 'flatReplyChain', name: 'Flatten Reply Indentation', description: 'Removes all indentation and vertical lines from quoted replies.', group: 'Email Thread Declutter', _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-flat-replies'; this._styleElement.textContent = ` /* kill all nested quote indents & vertical lines */ blockquote.gmail_quote, .gmail_quote, div.gmail_quote, .ii.gt blockquote { margin-left: 0 !important; padding-left: 0 !important; border-left: none !important; } /* ensure your separators span 100% */ hr { display: block !important; width: 100% !important; margin: 16px 0 !important; border: none !important; border-top: 1px solid #ccc !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); } }, { id: 'hideReactionButton', name: 'Hide "Add reaction" button', group: 'Email Thread Declutter', description: "Hides the emoji reaction button that appears on individual emails.", _styleElement: null, init() { this._styleElement = document.createElement('style'); this._styleElement.id = 'gm-hide-reactions'; this._styleElement.textContent = ` button[aria-label="Add reaction"], div.wrsVRe { display: none !important; } `; document.head.appendChild(this._styleElement); }, destroy() { this._styleElement?.remove(); }, }, { id: 'hideAllSignaturesInChain', name: 'Hide All Signatures in Reply Chain', description: 'Removes all standard, legacy, and device signatures from the entire reply chain.', init() { const hideSignatures = (rootNode) => { if (!rootNode.querySelectorAll) return; rootNode.querySelectorAll('blockquote td[style*="border-top"]').forEach(td => { const tbl = td.closest('table'); if (tbl && tbl.style.display !== 'none') { tbl.style.display = 'none'; tbl.dataset.gmHiddenBy = this.id; } }); rootNode.querySelectorAll('blockquote .gmail_signature_prefix, blockquote .gmail_signature').forEach(el => { if (el.style.display !== 'none') { el.style.display = 'none'; el.dataset.gmHiddenBy = this.id; } }); function hideNodeAndPrevBr(node) { if (node.style.display !== 'none') { node.style.display = 'none'; node.dataset.gmHiddenBy = 'hideAllSignaturesInChain'; } const prev = node.previousElementSibling; if (prev && prev.tagName === 'BR' && prev.style.display !== 'none') { prev.style.display = 'none'; prev.dataset.gmHiddenBy = 'hideAllSignaturesInChain'; } } rootNode.querySelectorAll('div[dir="ltr"]').forEach(div => { if (div.textContent.trim() === 'Sent from my iPhone') { hideNodeAndPrevBr(div); } }); rootNode.querySelectorAll('blockquote a').forEach(a => { if (/^Sent from .*Mail for iPhone$/i.test(a.textContent.trim())) { const container = a.closest('div') || a; hideNodeAndPrevBr(container); } }); rootNode.querySelectorAll('blockquote div[id*="ms-outlook-mobile-signature"], blockquote div:has(> a[href*="aka.ms/"])').forEach(el => { if (el.style.display !== 'none') { el.style.display = 'none'; el.dataset.gmHiddenBy = this.id; } }); }; addHidingRule(this.id, hideSignatures); }, destroy() { removeHidingRule(this.id); }, } ]; // ————————————————————— // 3. DOM HELPERS & TOAST NOTIFICATIONS // ————————————————————— let appState = {}; function createCogSvg() { const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("fill", "currentColor"); svg.setAttribute("width", "24"); svg.setAttribute("height", "24"); const path1 = document.createElementNS(svgNS, "path"); path1.setAttribute("d", "M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69-.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"); svg.appendChild(path1); return svg; } function showToast(message, isError = false) { let toast = document.getElementById('gm-toast-notification'); if (toast) toast.remove(); toast = document.createElement('div'); toast.id = 'gm-toast-notification'; toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background-color: ${isError ? '#d9534f' : '#5cb85c'}; color: white; padding: 10px 20px; border-radius: 5px; z-index: 10002; opacity: 0; transition: opacity 0.3s, bottom 0.3s; `; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '1'; toast.style.bottom = '30px'; }, 10); setTimeout(() => { toast.style.opacity = '0'; toast.style.bottom = '20px'; toast.addEventListener('transitionend', () => toast.remove()); }, 3000); } // ————————————————————— // 4. UI & SETTINGS PANEL // ————————————————————— function buildPanel(appState) { const groups = features.reduce((acc, f) => { acc[f.group] = acc[f.group] || []; acc[f.group].push(f); return acc; }, {}); const panelContainer = document.createElement('div'); panelContainer.id = 'gm-panel-container'; const overlay = document.createElement('div'); overlay.className = 'gm-panel-overlay'; overlay.onclick = () => document.body.classList.remove('gm-panel-open'); const panel = document.createElement('div'); panel.className = 'gm-panel'; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-labelledby', 'gm-panel-title'); const header = document.createElement('header'); const title = document.createElement('h2'); title.id = 'gm-panel-title'; title.textContent = 'Gmail Premium Suite'; const version = document.createElement('span'); version.className = 'version'; version.textContent = 'v6.9'; header.append(title, version); const main = document.createElement('main'); const groupOrder = ['UI & Visuals', 'Themes', 'Layout', 'Header Elements', 'Email Thread Declutter', 'Productivity', 'Hubspot', 'AI & Tools', 'Declutter']; const createSubSetting = (id, name, description, parentInput, parentFeatureId) => { const wrapper = document.createElement('div'); wrapper.className = 'gm-switch-wrapper gm-sub-setting-wrapper'; wrapper.dataset.tooltip = description; const label = document.createElement('label'); label.className = 'gm-switch'; label.htmlFor = `switch-${id}`; const input = document.createElement('input'); input.type = 'checkbox'; input.id = `switch-${id}`; input.checked = appState.settings[id]; input.onchange = async (e) => { appState.settings[id] = e.target.checked; const parentFeat = features.find(x => x.id === parentFeatureId); if (parentFeat) { parentFeat.destroy(); if (appState.settings[parentFeatureId]) { parentFeat.init(); } } await settingsManager.save(appState.settings); }; const slider = document.createElement('span'); slider.className = 'slider'; const nameSpan = document.createElement('span'); nameSpan.className = 'label'; nameSpan.textContent = name; label.append(input, slider); wrapper.append(label, nameSpan); wrapper.style.display = parentInput.checked ? 'flex' : 'none'; parentInput.addEventListener('change', (e) => { wrapper.style.display = e.target.checked ? 'flex' : 'none'; }); return wrapper; }; groupOrder.forEach(groupName => { if (!groups[groupName] || groups[groupName].length === 0) return; const fieldset = document.createElement('fieldset'); fieldset.className = 'gm-feature-group'; const legend = document.createElement('legend'); legend.textContent = groupName; fieldset.appendChild(legend); groups[groupName].forEach(f => { const wrapper = document.createElement('div'); wrapper.className = 'gm-switch-wrapper'; wrapper.dataset.tooltip = f.description; const label = document.createElement('label'); label.className = 'gm-switch'; label.htmlFor = `switch-${f.id}`; const input = document.createElement('input'); input.type = 'checkbox'; input.id = `switch-${f.id}`; input.dataset.featureId = f.id; input.checked = appState.settings[f.id]; input.onchange = async (e) => { const id = e.target.dataset.featureId; appState.settings[id] = e.target.checked; const feat = features.find(x => x.id === id); if (feat.destroy) { feat.destroy(); } if (appState.settings[id]) { // For dark mode, preInit is handled separately on page load, // so here we only need to call init. if (feat.init) { feat.init(); } } await settingsManager.save(appState.settings); }; const slider = document.createElement('span'); slider.className = 'slider'; const nameSpan = document.createElement('span'); nameSpan.className = 'label'; nameSpan.textContent = f.name; label.append(input, slider); wrapper.append(label, nameSpan); fieldset.appendChild(wrapper); if (f.id === 'gmailDarkMode') { const paneSub = createSubSetting('gmailDarkModePane', "Darken email viewing pane", "Applies the dark theme to the content of individual emails.", input, 'gmailDarkMode'); fieldset.append(paneSub); } if (f.id === 'nukeReplyMetadata') { const fromSub = createSubSetting('nukeReplyMetadataSimple', "Show simple 'From:' header", "Replaces the divider with a simple 'From: Name ' line.", input, 'nukeReplyMetadata'); const ccSub = createSubSetting('nukeReplyMetadataShowCc', "Show Cc:", "Also show the Cc: line if available.", input, 'nukeReplyMetadata'); const bccSub = createSubSetting('nukeReplyMetadataShowBcc', "Show Bcc:", "Also show the Bcc: line if available.", input, 'nukeReplyMetadata'); const pleasantriesSub = createSubSetting('nukeReplyMetadataRemovePleasantries', "Remove pleasantries (e.g., 'Thanks')", "Removes common closings from the end of emails.", input, 'nukeReplyMetadata'); fieldset.append(fromSub, ccSub, bccSub, pleasantriesSub); } }); main.appendChild(fieldset); }); const footer = document.createElement('footer'); const footerControls = document.createElement('div'); footerControls.className = 'gm-footer-controls'; const exportBtn = document.createElement('button'); exportBtn.textContent = 'Export Settings'; exportBtn.className = 'gm-btn-secondary'; exportBtn.onclick = () => { const settingsJson = JSON.stringify(appState.settings, null, 2); const blob = new Blob([settingsJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'gmail-premium-settings.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('Settings exported!'); }; const importBtn = document.createElement('button'); importBtn.textContent = 'Import Settings'; importBtn.className = 'gm-btn-secondary'; importBtn.onclick = () => { document.getElementById('gm-import-input').click(); }; const importInput = document.createElement('input'); importInput.type = 'file'; importInput.id = 'gm-import-input'; importInput.accept = '.json'; importInput.style.display = 'none'; importInput.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (event) => { try { const importedSettings = JSON.parse(event.target.result); if (typeof importedSettings.settingsButton !== 'boolean') { throw new Error("Invalid settings file."); } await settingsManager.save(importedSettings); showToast('Settings imported. Page will now reload.'); setTimeout(() => window.location.reload(), 1500); } catch (err) { console.error("Failed to import settings:", err); showToast('Error: Invalid or corrupt settings file.', true); } }; reader.readAsText(file); }; footerControls.append(importInput, importBtn, exportBtn); const closeBtn = document.createElement('button'); closeBtn.id = 'gm-close-btn'; closeBtn.className = 'gm-btn-primary'; closeBtn.textContent = 'Close'; closeBtn.onclick = () => document.body.classList.remove('gm-panel-open'); footer.append(footerControls, closeBtn); panel.append(header, main, footer); panelContainer.append(overlay, panel); document.body.appendChild(panelContainer); } // ————————————————————— // 5. STYLES // ————————————————————— function injectStyles() { const style = document.createElement('style'); style.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap'); :root { --panel-font: 'Inter', sans-serif; --panel-radius: 12px; --panel-shadow: 0 10px 30px -5px rgba(0,0,0,0.3); --gm-panel-bg: #2c2c2c; --gm-panel-fg: #f0f0f0; --gm-border-color: #444; --gm-accent-color: #5e97f6; } body.gm-panel-open #gm-panel-container .gm-panel-overlay { opacity: 1; pointer-events: auto; } .gm-panel-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 10000; opacity: 0; pointer-events: none; transition: opacity .3s ease; } .gm-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.95); width: 90%; max-width: 520px; background: var(--gm-panel-bg, #2c2c2c); color: var(--gm-panel-fg, #f0f0f0); border-radius: var(--panel-radius); box-shadow: var(--panel-shadow); font-family: var(--panel-font); opacity: 0; pointer-events: none; transition: opacity .3s ease, transform .3s ease; z-index: 10001; display: flex; flex-direction: column; } body.gm-panel-open .gm-panel { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); } .gm-panel header { padding: 20px 24px; border-bottom: 1px solid var(--gm-border-color, #444); display: flex; justify-content: space-between; align-items: center; } .gm-panel h2 { margin: 0; font-size: 18px; font-weight: 700; } .gm-panel .version { font-size: 12px; opacity: 0.6; } .gm-panel main { padding: 16px 24px; flex-grow: 1; max-height: 70vh; overflow-y: auto; } .gm-panel footer { padding: 16px 24px; border-top: 1px solid var(--gm-border-color, #444); display: flex; justify-content: space-between; align-items: center; } .gm-feature-group { border: 1px solid var(--gm-border-color, #444); border-radius: 8px; padding: 16px; margin: 0 0 16px; } .gm-feature-group legend { padding: 0 8px; font-size: 14px; font-weight: 500; color: var(--gm-accent-color, #5e97f6); } .gm-switch-wrapper { display: flex; align-items: center; margin-bottom: 12px; position: relative; } .gm-switch-wrapper:last-child { margin-bottom: 0; } .gm-switch { display: flex; align-items: center; cursor: pointer; } .gm-switch-wrapper .label { margin-left: 12px; flex: 1; font-size: 15px; } .gm-switch input { display: none; } .gm-switch .slider { width: 40px; height: 22px; background: #555; border-radius: 11px; position: relative; transition: background .2s ease; } .gm-switch .slider:before { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform .2s ease; } .gm-switch input:checked + .slider { background: var(--gm-accent-color, #5e97f6); } .gm-switch input:checked + .slider:before { transform: translateX(18px); } .gm-switch-wrapper::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 8px; background: #111; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 1; } .gm-switch-wrapper:hover::after { opacity: 1; } .gm-sub-setting-wrapper { margin-left: 20px; } .gm-footer-controls { display: flex; gap: 10px; } .gm-btn-primary { background-color: var(--gm-accent-color, #5e97f6); color: white; border: none; padding: 10px 20px; border-radius: 6px; font-family: var(--panel-font); font-weight: 500; cursor: pointer; transition: background-color .2s; } .gm-btn-primary:hover { background-color: #4a80d3; } .gm-btn-secondary { background-color: #444; color: #ddd; border: 1px solid #666; padding: 8px 12px; border-radius: 6px; font-size: 13px; font-family: var(--panel-font); cursor: pointer; transition: background-color .2s, border-color .2s; } .gm-btn-secondary:hover { background-color: #555; border-color: #777; } .gm-btn-secondary.active { background-color: #5e97f6; border-color: #5e97f6; } #gm-floating-settings-btn { position: fixed; bottom: 24px; left: 24px; width: 56px; height: 56px; background-color: var(--gm-accent-color, #5e97f6); color: white; border: none; border-radius: 50%; box-shadow: 0 4px 12px rgba(0,0,0,0.2); cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 9999; transition: transform .2s ease, background-color .2s ease; } #gm-floating-settings-btn:hover { transform: scale(1.05); background-color: #4a80d3; } `; document.head.appendChild(style); } // ————————————————————— // 6. MAIN BOOTSTRAP // ————————————————————— async function main() { appState.settings = await settingsManager.load(); // Because @run-at is document-start, run the dark mode PRE-INITIALIZER // immediately to apply the dark loading screen and prevent flashing. if (appState.settings.gmailDarkMode) { const darkModeFeat = features.find(f => f.id === 'gmailDarkMode'); if (darkModeFeat && darkModeFeat.preInit) { darkModeFeat.preInit(); } } const bootstrapObserver = new MutationObserver((mutations, obs) => { if (document.querySelector('.nH.bkK')) { obs.disconnect(); // Initialize all features once the main UI is ready injectStyles(); buildPanel(appState); features.forEach(f => { // Now, the main init() for all enabled features can run safely if (appState.settings[f.id]) { try { if (f.init) f.init(); } catch (error) { console.error(`[Gmail Premium] Error initializing feature "${f.id}":`, error); } } }); // Show the panel on first run settingsManager.getFirstRunStatus().then(hasRun => { if (!hasRun) { document.body.classList.add('gm-panel-open'); settingsManager.setFirstRunStatus(true); } }); } }); bootstrapObserver.observe(document.body, { childList: true, subtree: true }); } main().catch(error => { console.error("[Gmail Premium] Failed to initialize:", error); }); })();