theme.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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. getRoot().dataset.theme = theme
  21. }
  22. export const useThemeStore = defineStore('theme', () => {
  23. const session = useSessionStore()
  24. const savingTheme = ref(false)
  25. const setThemeMutation = new MutationHandler(
  26. useUserCurrentAppearanceMutation(),
  27. {
  28. errorNotificationMessage: __('The appearance could not be updated.'),
  29. },
  30. )
  31. const saveTheme = (newTheme: AppThemeName) => {
  32. const sanitizedTheme = sanitizeTheme(newTheme)
  33. saveDOMTheme(sanitizedTheme)
  34. }
  35. const setTheme = async (theme: string) => {
  36. const oldTheme = session.user?.preferences?.theme
  37. session.setUserPreference('theme', theme)
  38. return setThemeMutation
  39. .send({ theme: theme as EnumAppearanceTheme })
  40. .catch(() => {
  41. session.setUserPreference('theme', oldTheme)
  42. })
  43. }
  44. const currentTheme = computed(
  45. () => session.user?.preferences?.theme || 'auto',
  46. )
  47. const isDarkMode = computed(
  48. () => sanitizeTheme(currentTheme.value) === 'dark',
  49. )
  50. const updateTheme = async (value: EnumAppearanceTheme) => {
  51. try {
  52. if (value === session.user?.preferences?.theme || savingTheme.value)
  53. return
  54. savingTheme.value = true
  55. await setTheme(value)
  56. } finally {
  57. savingTheme.value = false
  58. }
  59. }
  60. // sync theme in case HTML value was not up-to-date when we loaded user preferences
  61. const syncTheme = () => {
  62. if (currentTheme.value !== 'auto') return saveDOMTheme(currentTheme.value)
  63. const theme = getPreferredTheme()
  64. saveDOMTheme(theme)
  65. }
  66. // Update based on global system level preference
  67. window
  68. .matchMedia('(prefers-color-scheme: dark)')
  69. .addEventListener('change', () => {
  70. // don't override preferred theme if user has already selected one
  71. const theme = (currentTheme.value as AppThemeName) || getPreferredTheme()
  72. saveTheme(theme)
  73. })
  74. // in case user changes the theme in another tab
  75. watch(
  76. () => currentTheme.value,
  77. (newTheme) => {
  78. if (newTheme) {
  79. saveTheme(newTheme as AppThemeName)
  80. }
  81. },
  82. )
  83. return {
  84. savingTheme,
  85. currentTheme,
  86. isDarkMode,
  87. updateTheme,
  88. syncTheme,
  89. }
  90. })
  91. if (import.meta.hot) {
  92. import.meta.hot.accept(acceptHMRUpdate(useThemeStore, import.meta.hot))
  93. }