theme.ts 3.6 KB

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