huhuih
hzgjuigik
This commit is contained in:
186
client/src/hooks/useTheme.ts
Normal file
186
client/src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Theme Hook
|
||||
* Handles dark mode detection and Dark Reader compatibility
|
||||
* Uses MutationObserver for efficient Dark Reader detection
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useTheme() {
|
||||
const [isDark, setIsDark] = useState(false)
|
||||
const [hasDarkReader, setHasDarkReader] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const html = document.documentElement
|
||||
|
||||
// Helper function to apply/remove dark mode
|
||||
const applyDarkMode = (shouldBeDark: boolean) => {
|
||||
setIsDark(shouldBeDark)
|
||||
if (shouldBeDark) {
|
||||
html.classList.add('dark')
|
||||
html.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
html.classList.remove('dark')
|
||||
html.setAttribute('data-theme', 'light')
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Dark Reader detection with multiple methods
|
||||
const detectDarkReader = (): boolean => {
|
||||
// Method 1: Check for Dark Reader data attributes on html element
|
||||
const hasDarkReaderAttributes =
|
||||
html.hasAttribute('data-darkreader-mode') ||
|
||||
html.hasAttribute('data-darkreader-scheme') ||
|
||||
html.hasAttribute('data-darkreader-policy')
|
||||
|
||||
// Method 2: Check for Dark Reader stylesheet or meta tags
|
||||
const hasDarkReaderMeta =
|
||||
document.querySelector('meta[name="darkreader"]') !== null ||
|
||||
document.querySelector('style[data-darkreader]') !== null
|
||||
|
||||
// Method 3: Check computed styles for filter/invert (Dark Reader uses CSS filters)
|
||||
const computedStyle = window.getComputedStyle(html)
|
||||
const hasFilter = computedStyle.filter && computedStyle.filter !== 'none'
|
||||
const hasInvert = computedStyle.filter?.includes('invert') ||
|
||||
computedStyle.filter?.includes('brightness')
|
||||
|
||||
// Method 4: Check for Dark Reader's characteristic background color
|
||||
// Dark Reader often sets a specific dark background
|
||||
const bgColor = computedStyle.backgroundColor
|
||||
const isDarkReaderBg = bgColor === 'rgb(24, 26, 27)' ||
|
||||
bgColor === 'rgb(18, 18, 18)' ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches &&
|
||||
bgColor !== 'rgba(0, 0, 0, 0)' &&
|
||||
bgColor !== 'transparent' &&
|
||||
!html.classList.contains('dark'))
|
||||
|
||||
// Method 5: Check for Dark Reader injected styles
|
||||
const styleSheets = Array.from(document.styleSheets)
|
||||
const hasDarkReaderStylesheet = styleSheets.some(sheet => {
|
||||
try {
|
||||
const href = sheet.href || ''
|
||||
return href.includes('darkreader') ||
|
||||
(sheet.ownerNode as Element)?.getAttribute('data-darkreader') !== null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return hasDarkReaderAttributes ||
|
||||
hasDarkReaderMeta ||
|
||||
(hasFilter && hasInvert) ||
|
||||
isDarkReaderBg ||
|
||||
hasDarkReaderStylesheet
|
||||
}
|
||||
|
||||
// Check system preference
|
||||
const checkSystemPreference = (): boolean => {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
// Update theme based on current state
|
||||
const updateTheme = () => {
|
||||
const darkReaderDetected = detectDarkReader()
|
||||
const systemPrefersDark = checkSystemPreference()
|
||||
|
||||
setHasDarkReader(darkReaderDetected)
|
||||
|
||||
// Only apply dark mode if system prefers it AND Dark Reader is not active
|
||||
if (systemPrefersDark && !darkReaderDetected) {
|
||||
applyDarkMode(true)
|
||||
} else {
|
||||
applyDarkMode(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Initial check
|
||||
updateTheme()
|
||||
|
||||
// Listen for system preference changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleSystemPreferenceChange = (e: MediaQueryListEvent) => {
|
||||
updateTheme()
|
||||
}
|
||||
|
||||
// Modern browsers
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', handleSystemPreferenceChange)
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
mediaQuery.addListener(handleSystemPreferenceChange)
|
||||
}
|
||||
|
||||
// MutationObserver for Dark Reader attribute changes (more efficient than setInterval)
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldUpdate = false
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
// Check if Dark Reader attributes were added/removed
|
||||
if (mutation.type === 'attributes') {
|
||||
const attrName = mutation.attributeName
|
||||
if (attrName?.startsWith('data-darkreader') ||
|
||||
attrName === 'class' ||
|
||||
attrName === 'data-theme') {
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Dark Reader elements were added/removed
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as Element
|
||||
if (el.hasAttribute?.('data-darkreader') ||
|
||||
el.tagName === 'META' && el.getAttribute('name') === 'darkreader' ||
|
||||
el.tagName === 'STYLE' && el.hasAttribute('data-darkreader')) {
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (shouldUpdate) {
|
||||
updateTheme()
|
||||
}
|
||||
})
|
||||
|
||||
// Observe html element for Dark Reader attribute changes
|
||||
observer.observe(html, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-darkreader-mode', 'data-darkreader-scheme', 'data-darkreader-policy', 'class', 'data-theme'],
|
||||
childList: true,
|
||||
subtree: false
|
||||
})
|
||||
|
||||
// Also observe document head for Dark Reader meta/stylesheets
|
||||
if (document.head) {
|
||||
observer.observe(document.head, {
|
||||
childList: true,
|
||||
subtree: false
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback: Periodic check (reduced frequency, only as safety net)
|
||||
// This catches edge cases where MutationObserver might miss something
|
||||
const fallbackInterval = setInterval(() => {
|
||||
const currentDarkReader = detectDarkReader()
|
||||
if (currentDarkReader !== hasDarkReader) {
|
||||
updateTheme()
|
||||
}
|
||||
}, 5000) // Check every 5 seconds (reduced from 2 seconds)
|
||||
|
||||
return () => {
|
||||
// Cleanup
|
||||
if (mediaQuery.removeEventListener) {
|
||||
mediaQuery.removeEventListener('change', handleSystemPreferenceChange)
|
||||
} else {
|
||||
mediaQuery.removeListener(handleSystemPreferenceChange)
|
||||
}
|
||||
observer.disconnect()
|
||||
clearInterval(fallbackInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { isDark, hasDarkReader }
|
||||
}
|
||||
Reference in New Issue
Block a user