RedditThemeStyle.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import Foundation
  2. enum RedditThemeStyle {
  3. static func earlyInjectionScript(isDarkMode: Bool) -> String {
  4. """
  5. (function() {
  6. const wantDark = \(isDarkMode ? "true" : "false");
  7. window.__reddoraTheme = { wantsDark: wantDark };
  8. const installMatchMediaOverride = (wantsDark) => {
  9. if (window.__reddoraTheme.matchMediaInstalled) return;
  10. const original = window.matchMedia.bind(window);
  11. window.matchMedia = function(query) {
  12. const result = original(query);
  13. if (typeof query === 'string' && query.includes('prefers-color-scheme')) {
  14. const matches = query.includes('dark') ? wantsDark : !wantsDark;
  15. return {
  16. matches,
  17. media: query,
  18. onchange: null,
  19. addEventListener: (...args) => result.addEventListener(...args),
  20. removeEventListener: (...args) => result.removeEventListener(...args),
  21. dispatchEvent: (...args) => result.dispatchEvent(...args),
  22. };
  23. }
  24. return result;
  25. };
  26. window.__reddoraTheme.matchMediaInstalled = true;
  27. };
  28. installMatchMediaOverride(wantDark);
  29. })();
  30. """
  31. }
  32. static func syncScript(isDarkMode: Bool) -> String {
  33. """
  34. (function() {
  35. const wantDark = \(isDarkMode ? "true" : "false");
  36. window.__reddoraTheme = window.__reddoraTheme || {};
  37. window.__reddoraTheme.wantsDark = wantDark;
  38. const installMatchMediaOverride = (wantsDark) => {
  39. const original = window.matchMedia.bind(window);
  40. window.matchMedia = function(query) {
  41. const result = original(query);
  42. if (typeof query === 'string' && query.includes('prefers-color-scheme')) {
  43. const matches = query.includes('dark') ? wantsDark : !wantsDark;
  44. return {
  45. matches,
  46. media: query,
  47. onchange: null,
  48. addEventListener: (...args) => result.addEventListener(...args),
  49. removeEventListener: (...args) => result.removeEventListener(...args),
  50. dispatchEvent: (...args) => result.dispatchEvent(...args),
  51. };
  52. }
  53. return result;
  54. };
  55. window.__reddoraTheme.matchMediaInstalled = true;
  56. };
  57. installMatchMediaOverride(wantDark);
  58. const isDrawerHidden = (element) => !element || (element.offsetWidth === 0 && element.offsetHeight === 0);
  59. const isRedditDarkModeActive = () => {
  60. const header = document.querySelector('header');
  61. const themedNode = header?.firstElementChild;
  62. if (themedNode) {
  63. const background = getComputedStyle(themedNode)
  64. .getPropertyValue('--shreddit-content-background')
  65. .trim()
  66. .toLowerCase();
  67. if (background === '#0e1113' || background === '#1a1a1b') return true;
  68. if (background === '#ffffff' || background === '#fff') return false;
  69. }
  70. const root = document.documentElement;
  71. if (root.classList.contains('theme-dark')) return true;
  72. if (root.classList.contains('theme-light')) return false;
  73. return window.matchMedia('(prefers-color-scheme: dark)').matches;
  74. };
  75. const toggleViaDrawer = () => {
  76. const drawerButton = document.querySelector('#expand-user-drawer-button');
  77. const drawer = document.querySelector('#user-drawer-content');
  78. const switchInput = document.querySelector(
  79. '#darkmode-list-item faceplate-switch-input[name="darkmode-switch-name"]'
  80. );
  81. if (!drawerButton || !drawer || !switchInput) return false;
  82. const drawerWasHidden = isDrawerHidden(drawer);
  83. if (drawerWasHidden) drawerButton.click();
  84. const isChecked = switchInput.checked === true
  85. || switchInput.getAttribute('checked') !== null
  86. || switchInput.getAttribute('aria-checked') === 'true';
  87. if (isChecked !== wantDark) {
  88. switchInput.click();
  89. }
  90. if (drawerWasHidden) {
  91. setTimeout(() => {
  92. if (!isDrawerHidden(drawer)) drawerButton.click();
  93. }, 250);
  94. }
  95. return true;
  96. };
  97. const syncViaAPI = async () => {
  98. try {
  99. const response = await fetch('/api/v1/me/prefs', {
  100. method: 'PATCH',
  101. headers: { 'Content-Type': 'application/json' },
  102. credentials: 'same-origin',
  103. body: JSON.stringify({ nightmode: wantDark }),
  104. });
  105. return response.ok;
  106. } catch {
  107. return false;
  108. }
  109. };
  110. const run = async () => {
  111. const current = isRedditDarkModeActive();
  112. if (current === wantDark) return;
  113. if (toggleViaDrawer()) return;
  114. const updated = await syncViaAPI();
  115. if (updated) {
  116. window.location.reload();
  117. }
  118. };
  119. if (document.readyState === 'loading') {
  120. document.addEventListener('DOMContentLoaded', () => run(), { once: true });
  121. } else {
  122. run();
  123. }
  124. })();
  125. """
  126. }
  127. }