ProtonMail Dark Theme for Email Content

Adds dark theme to ProtonMail email reading and composition areas

  1. // ==UserScript==
  2. // @name ProtonMail Dark Theme for Email Content
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5
  5. // @description Adds dark theme to ProtonMail email reading and composition areas
  6. // @match https://mail.proton.me/*
  7. // @match https://proton.me/mail/*
  8. // @grant GM_addStyle
  9. // @grant unsafeWindow
  10. // @license WTFPL
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Main window CSS
  17. GM_addStyle(`
  18. /* Composer window and borders */
  19. .composer, .composer .composer-content--rich-edition {
  20. background-color: #1a1a1a !important;
  21. border-color: #333 !important;
  22. }
  23.  
  24. .composer-inner {
  25. background-color: #1a1a1a !important;
  26. border-color: #333 !important;
  27. }
  28.  
  29. .composer-title {
  30. background-color: #1a1a1a !important;
  31. border-color: #333 !important;
  32. }
  33.  
  34. /* Message containers */
  35. .message-container,
  36. .message-content,
  37. .message-content-wrapper,
  38. .scroll-horizontal-shadow {
  39. background-color: #1a1a1a !important;
  40. border-color: #333 !important;
  41. }
  42.  
  43. /* General containers and borders */
  44. .rounded-lg,
  45. .border,
  46. [class*="border-"] {
  47. border-color: #333 !important;
  48. }
  49.  
  50. /* Message header and footer areas */
  51. .message-header-wrapper,
  52. .message-footer-wrapper {
  53. background-color: #1a1a1a !important;
  54. }
  55.  
  56. /* Toolbar and button backgrounds */
  57. .editor-toolbar,
  58. .composer-toolbar {
  59. background-color: #262626 !important;
  60. border-color: #333 !important;
  61. }
  62. `);
  63.  
  64. // CSS to inject into iframes
  65. const darkThemeCSS = `
  66. body {
  67. background-color: #1a1a1a !important;
  68. color: #e0e0e0 !important;
  69. }
  70.  
  71. p, div, span, h1, h2, h3, h4, h5, h6, td, th, li {
  72. color: #e0e0e0 !important;
  73. background-color: #1a1a1a !important;
  74. }
  75.  
  76. [style*="background-color"],
  77. [bgcolor] {
  78. background-color: #1a1a1a !important;
  79. }
  80.  
  81. [style*="color"],
  82. [color] {
  83. color: #e0e0e0 !important;
  84. }
  85.  
  86. blockquote {
  87. border-left-color: #404040 !important;
  88. background-color: #262626 !important;
  89. color: #e0e0e0 !important;
  90. }
  91.  
  92. a {
  93. color: #66b3ff !important;
  94. }
  95.  
  96. /* Table backgrounds */
  97. table, tr, td, th {
  98. background-color: #1a1a1a !important;
  99. border-color: #333 !important;
  100. }
  101.  
  102. /* Force color on common elements that might have inline styles */
  103. [style*="color: rgb(0, 0, 0)"],
  104. [style*="color: black"],
  105. [style*="color:#000"],
  106. [style*="color: #000"],
  107. font[color] {
  108. color: #e0e0e0 !important;
  109. }
  110.  
  111. .composer {
  112. background-color: #1a1a1a !important;
  113. border-color: #333 !important;
  114. }
  115.  
  116. .composer-inner {
  117. background-color: #1a1a1a !important;
  118. border-color: #333 !important;
  119. }
  120.  
  121. .composer-title {
  122. background-color: #1a1a1a !important;
  123. border-color: #333 !important;
  124. }
  125.  
  126. /* Message containers */
  127. .message-container,
  128. .message-content-wrapper,
  129. .scroll-horizontal-shadow {
  130. background-color: #1a1a1a !important;
  131. border-color: #333 !important;
  132. }
  133.  
  134. /* General containers and borders */
  135. .rounded-lg,
  136. .border,
  137. [class*="border-"] {
  138. border-color: #333 !important;
  139. }
  140.  
  141. /* Message header and footer areas */
  142. .message-header-wrapper,
  143. .message-footer-wrapper {
  144. background-color: #1a1a1a !important;
  145. }
  146.  
  147. /* Toolbar and button backgrounds */
  148. .editor-toolbar,
  149. .composer-toolbar {
  150. background-color: #262626 !important;
  151. border-color: #333 !important;
  152. }
  153. `;
  154.  
  155. // Function to inject styles into an iframe
  156. function injectIframeStyles(iframe) {
  157. try {
  158. const inject = () => {
  159. if (!iframe.contentDocument) return;
  160.  
  161. // Create and inject stylesheet if it doesn't exist
  162. if (!iframe.contentDocument.querySelector('#dark-theme-style')) {
  163. const style = iframe.contentDocument.createElement('style');
  164. style.id = 'dark-theme-style';
  165. style.textContent = darkThemeCSS;
  166. iframe.contentDocument.head.appendChild(style);
  167. }
  168.  
  169. // Force color on any elements with inline styles
  170. const elements = iframe.contentDocument.querySelectorAll('*');
  171. elements.forEach(el => {
  172. if (el.tagName !== 'IMG') {
  173. el.style.setProperty('background-color', '#1a1a1a', 'important');
  174. el.style.setProperty('color', '#e0e0e0', 'important');
  175. if (el.style.backgroundImage && el.style.backgroundImage !== 'none') {
  176. el.style.setProperty('background-image', 'none', 'important');
  177. }
  178. }
  179. });
  180.  
  181. // Add mutation observer for dynamically added content within the iframe
  182. const observer = new MutationObserver((mutations) => {
  183. mutations.forEach((mutation) => {
  184. mutation.addedNodes.forEach((node) => {
  185. if (node.nodeType === 1 && node.tagName !== 'IMG') {
  186. node.style.setProperty('background-color', '#1a1a1a', 'important');
  187. node.style.setProperty('color', '#e0e0e0', 'important');
  188. }
  189. });
  190. });
  191. });
  192.  
  193. observer.observe(iframe.contentDocument.body, {
  194. childList: true,
  195. subtree: true,
  196. attributes: true,
  197. attributeFilter: ['style', 'class']
  198. });
  199. };
  200.  
  201. // If iframe is already loaded
  202. if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
  203. inject();
  204. }
  205.  
  206. // Also listen for load event
  207. iframe.addEventListener('load', inject);
  208.  
  209. } catch (e) {
  210. // Handle cross-origin errors silently
  211. }
  212. }
  213.  
  214. // Function to watch a specific container for iframes
  215. function watchContainer(container) {
  216. if (!container) return;
  217.  
  218. // Process any existing iframes
  219. container.querySelectorAll('iframe').forEach(iframe => {
  220. injectIframeStyles(iframe);
  221. });
  222.  
  223. // Watch for new iframes
  224. const observer = new MutationObserver((mutations) => {
  225. mutations.forEach(mutation => {
  226. mutation.addedNodes.forEach(node => {
  227. // Direct iframe
  228. if (node.tagName === 'IFRAME') {
  229. injectIframeStyles(node);
  230. }
  231. // Iframe within added node
  232. if (node.querySelectorAll) {
  233. node.querySelectorAll('iframe').forEach(iframe => {
  234. injectIframeStyles(iframe);
  235. });
  236. }
  237. });
  238. });
  239. });
  240.  
  241. observer.observe(container, {
  242. childList: true,
  243. subtree: true,
  244. attributes: true,
  245. attributeFilter: ['src', 'srcdoc']
  246. });
  247. }
  248.  
  249. // Function to set up all observers
  250. function setupObservers() {
  251. // Watch for new messages and composers
  252. const bodyObserver = new MutationObserver((mutations) => {
  253. mutations.forEach(mutation => {
  254. mutation.addedNodes.forEach(node => {
  255. if (node.nodeType === 1) {
  256. // Watch message containers
  257. if (node.classList?.contains('message-container')) {
  258. watchContainer(node);
  259. }
  260. // Watch for reply composer
  261. if (node.classList?.contains('reply-wrapper')) {
  262. watchContainer(node);
  263. }
  264. // Watch any other potential containers
  265. node.querySelectorAll('.message-container, .reply-wrapper, .composer-body-container').forEach(container => {
  266. watchContainer(container);
  267. });
  268. }
  269. });
  270. });
  271. });
  272.  
  273. bodyObserver.observe(document.body, {
  274. childList: true,
  275. subtree: true
  276. });
  277.  
  278. // Process existing containers
  279. document.querySelectorAll('.message-container, .reply-wrapper, .composer-body-container').forEach(container => {
  280. watchContainer(container);
  281. });
  282. }
  283.  
  284. // Initial setup
  285. setupObservers();
  286.  
  287. // Handle route changes
  288. let lastUrl = location.href;
  289. new MutationObserver(() => {
  290. const url = location.href;
  291. if (url !== lastUrl) {
  292. lastUrl = url;
  293. setTimeout(setupObservers, 500);
  294. }
  295. }).observe(document, { subtree: true, childList: true });
  296.  
  297. // Check periodically for new reply composers
  298. setInterval(() => {
  299. document.querySelectorAll('.reply-wrapper iframe').forEach(iframe => {
  300. if (!iframe.contentDocument?.querySelector('#dark-theme-style')) {
  301. injectIframeStyles(iframe);
  302. }
  303. });
  304. }, 1000);
  305.  
  306. })();