theme.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { acceptHMRUpdate, defineStore } from 'pinia'
  3. import { computed, ref, watch } from 'vue'
  4. import { EnumAppearanceTheme } from '#shared/graphql/types.ts'
  5. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  6. import { useSessionStore } from '#shared/stores/session.ts'
  7. import { useUserCurrentAppearanceMutation } from '#desktop/pages/personal-setting/graphql/mutations/userCurrentAppearance.api.ts'
  8. type AppThemeName = EnumAppearanceTheme.Light | EnumAppearanceTheme.Dark
  9. const getRoot = () => document.querySelector(':root') as HTMLElement
  10. const getPreferredTheme = (): AppThemeName => {
  11. return window.matchMedia('(prefers-color-scheme: dark)').matches
  12. ? EnumAppearanceTheme.Dark
  13. : EnumAppearanceTheme.Light
  14. }
  15. const sanitizeTheme = (theme: string): AppThemeName => {
  16. if (['dark', 'light'].includes(theme)) return theme as AppThemeName
  17. return getPreferredTheme()
  18. }
  19. const saveDOMTheme = (theme: AppThemeName) => {
  20. const root = getRoot()
  21. root.dataset.theme = theme
  22. root.style.colorScheme = theme
  23. }
  24. export const useThemeStore = defineStore('theme', () => {
  25. const session = useSessionStore()
  26. const savingTheme = ref(false)
  27. const setThemeMutation = new MutationHandler(
  28. useUserCurrentAppearanceMutation(),
  29. {
  30. errorNotificationMessage: __('The appearance could not be updated.'),
  31. },
  32. )
  33. const saveTheme = (newTheme: AppThemeName) => {
  34. const sanitizedTheme = sanitizeTheme(newTheme)
  35. saveDOMTheme(sanitizedTheme)
  36. }
  37. const setTheme = async (theme: string) => {
  38. const oldTheme = session.user?.preferences?.theme
  39. session.setUserPreference('theme', theme)
  40. return setThemeMutation
  41. .send({ theme: theme as EnumAppearanceTheme })
  42. .catch(() => {
  43. session.setUserPreference('theme', oldTheme)
  44. })
  45. }
  46. const currentTheme = computed(
  47. () => session.user?.preferences?.theme || 'auto',
  48. )
  49. const isDarkMode = computed(
  50. () => sanitizeTheme(currentTheme.value) === 'dark',
  51. )
  52. const updateTheme = async (value: EnumAppearanceTheme) => {
  53. try {
  54. if (value === session.user?.preferences?.theme || savingTheme.value)
  55. return
  56. savingTheme.value = true
  57. await setTheme(value)
  58. } finally {
  59. savingTheme.value = false
  60. }
  61. }
  62. // sync theme in case HTML value was not up-to-date when we loaded user preferences
  63. const syncTheme = () => {
  64. if (currentTheme.value !== 'auto') return saveDOMTheme(currentTheme.value)
  65. const theme = getPreferredTheme()
  66. saveDOMTheme(theme)
  67. }
  68. // Update based on global system level preference
  69. window
  70. .matchMedia('(prefers-color-scheme: dark)')
  71. .addEventListener('change', () => {
  72. // don't override preferred theme if user has already selected one
  73. const theme = (currentTheme.value as AppThemeName) || getPreferredTheme()
  74. saveTheme(theme)
  75. })
  76. // in case user changes the theme in another tab
  77. watch(
  78. () => currentTheme.value,
  79. (newTheme) => {
  80. if (newTheme) {
  81. saveTheme(newTheme as AppThemeName)
  82. }
  83. },
  84. )
  85. return {
  86. savingTheme,
  87. currentTheme,
  88. isDarkMode,
  89. updateTheme,
  90. syncTheme,
  91. }
  92. })
  93. if (import.meta.hot) {
  94. import.meta.hot.accept(acceptHMRUpdate(useThemeStore, import.meta.hot))
  95. }