theme.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  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 updateTheme = async (value: EnumAppearanceTheme) => {
  48. try {
  49. if (value === session.user?.preferences?.theme || savingTheme.value)
  50. return
  51. savingTheme.value = true
  52. await setTheme(value)
  53. } finally {
  54. savingTheme.value = false
  55. }
  56. }
  57. // sync theme in case HTML value was not up-to-date when we loaded user preferences
  58. const syncTheme = () => {
  59. if (currentTheme.value !== 'auto') return saveDOMTheme(currentTheme.value)
  60. const theme = getPreferredTheme()
  61. saveDOMTheme(theme)
  62. }
  63. // Update based on global system level preference
  64. window
  65. .matchMedia('(prefers-color-scheme: dark)')
  66. .addEventListener('change', () => {
  67. // don't override preferred theme if user has already selected one
  68. const theme = (currentTheme.value as AppThemeName) || getPreferredTheme()
  69. saveTheme(theme)
  70. })
  71. // in case user changes the theme in another tab
  72. watch(
  73. () => currentTheme.value,
  74. (newTheme) => {
  75. if (newTheme) {
  76. saveTheme(newTheme as AppThemeName)
  77. }
  78. },
  79. )
  80. return {
  81. savingTheme,
  82. currentTheme,
  83. updateTheme,
  84. syncTheme,
  85. }
  86. })
  87. if (import.meta.hot) {
  88. import.meta.hot.accept(acceptHMRUpdate(useThemeStore, import.meta.hot))
  89. }