appearance.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. // SiYuan - Refactor your thinking
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package model
  17. import (
  18. "fmt"
  19. "os"
  20. "path/filepath"
  21. "strings"
  22. "sync"
  23. "time"
  24. "github.com/88250/gulu"
  25. "github.com/fsnotify/fsnotify"
  26. "github.com/siyuan-note/filelock"
  27. "github.com/siyuan-note/logging"
  28. "github.com/siyuan-note/siyuan/kernel/bazaar"
  29. "github.com/siyuan-note/siyuan/kernel/util"
  30. )
  31. func InitAppearance() {
  32. util.SetBootDetails("Initializing appearance...")
  33. if err := os.Mkdir(util.AppearancePath, 0755); nil != err && !os.IsExist(err) {
  34. logging.LogErrorf("create appearance folder [%s] failed: %s", util.AppearancePath, err)
  35. util.ReportFileSysFatalError(err)
  36. return
  37. }
  38. unloadThemes()
  39. from := filepath.Join(util.WorkingDir, "appearance")
  40. if err := filelock.Copy(from, util.AppearancePath); nil != err {
  41. logging.LogErrorf("copy appearance resources from [%s] to [%s] failed: %s", from, util.AppearancePath, err)
  42. util.ReportFileSysFatalError(err)
  43. return
  44. }
  45. loadThemes()
  46. if !gulu.Str.Contains(Conf.Appearance.ThemeDark, Conf.Appearance.DarkThemes) {
  47. Conf.Appearance.ThemeDark = "midnight"
  48. Conf.Appearance.ThemeJS = false
  49. }
  50. if !gulu.Str.Contains(Conf.Appearance.ThemeLight, Conf.Appearance.LightThemes) {
  51. Conf.Appearance.ThemeLight = "daylight"
  52. Conf.Appearance.ThemeJS = false
  53. }
  54. loadIcons()
  55. if !gulu.Str.Contains(Conf.Appearance.Icon, Conf.Appearance.Icons) {
  56. Conf.Appearance.Icon = "material"
  57. }
  58. Conf.Save()
  59. }
  60. var themeWatchers = sync.Map{} // [string]*fsnotify.Watcher{}
  61. func closeThemeWatchers() {
  62. themeWatchers.Range(func(key, value interface{}) bool {
  63. if err := value.(*fsnotify.Watcher).Close(); nil != err {
  64. logging.LogErrorf("close file watcher failed: %s", err)
  65. }
  66. return true
  67. })
  68. }
  69. func unloadThemes() {
  70. if !util.IsPathRegularDirOrSymlinkDir(util.ThemesPath) {
  71. return
  72. }
  73. themeDirs, err := os.ReadDir(util.ThemesPath)
  74. if nil != err {
  75. logging.LogErrorf("read appearance themes folder failed: %s", err)
  76. return
  77. }
  78. for _, themeDir := range themeDirs {
  79. if !util.IsDirRegularOrSymlink(themeDir) {
  80. continue
  81. }
  82. unwatchTheme(filepath.Join(util.ThemesPath, themeDir.Name()))
  83. }
  84. }
  85. func loadThemes() {
  86. themeDirs, err := os.ReadDir(util.ThemesPath)
  87. if nil != err {
  88. logging.LogErrorf("read appearance themes folder failed: %s", err)
  89. util.ReportFileSysFatalError(err)
  90. return
  91. }
  92. Conf.Appearance.DarkThemes = nil
  93. Conf.Appearance.LightThemes = nil
  94. for _, themeDir := range themeDirs {
  95. if !util.IsDirRegularOrSymlink(themeDir) {
  96. continue
  97. }
  98. name := themeDir.Name()
  99. themeConf, parseErr := bazaar.ThemeJSON(name)
  100. if nil != parseErr || nil == themeConf {
  101. continue
  102. }
  103. modes := themeConf.Modes
  104. for _, mode := range modes {
  105. if "dark" == mode {
  106. Conf.Appearance.DarkThemes = append(Conf.Appearance.DarkThemes, name)
  107. } else if "light" == mode {
  108. Conf.Appearance.LightThemes = append(Conf.Appearance.LightThemes, name)
  109. }
  110. }
  111. if 0 == Conf.Appearance.Mode {
  112. if Conf.Appearance.ThemeLight == name {
  113. Conf.Appearance.ThemeVer = themeConf.Version
  114. Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
  115. }
  116. } else {
  117. if Conf.Appearance.ThemeDark == name {
  118. Conf.Appearance.ThemeVer = themeConf.Version
  119. Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
  120. }
  121. }
  122. go watchTheme(filepath.Join(util.ThemesPath, name))
  123. }
  124. }
  125. func loadIcons() {
  126. iconDirs, err := os.ReadDir(util.IconsPath)
  127. if nil != err {
  128. logging.LogErrorf("read appearance icons folder failed: %s", err)
  129. util.ReportFileSysFatalError(err)
  130. return
  131. }
  132. Conf.Appearance.Icons = nil
  133. for _, iconDir := range iconDirs {
  134. if !util.IsDirRegularOrSymlink(iconDir) {
  135. continue
  136. }
  137. name := iconDir.Name()
  138. iconConf, err := bazaar.IconJSON(name)
  139. if nil != err || nil == iconConf {
  140. continue
  141. }
  142. Conf.Appearance.Icons = append(Conf.Appearance.Icons, name)
  143. if Conf.Appearance.Icon == name {
  144. Conf.Appearance.IconVer = iconConf.Version
  145. }
  146. }
  147. }
  148. func unwatchTheme(folder string) {
  149. val, _ := themeWatchers.Load(folder)
  150. if nil != val {
  151. themeWatcher := val.(*fsnotify.Watcher)
  152. themeWatcher.Close()
  153. }
  154. }
  155. func watchTheme(folder string) {
  156. val, _ := themeWatchers.Load(folder)
  157. var themeWatcher *fsnotify.Watcher
  158. if nil != val {
  159. themeWatcher = val.(*fsnotify.Watcher)
  160. themeWatcher.Close()
  161. }
  162. var err error
  163. if themeWatcher, err = fsnotify.NewWatcher(); nil != err {
  164. logging.LogErrorf("add theme file watcher for folder [%s] failed: %s", folder, err)
  165. return
  166. }
  167. themeWatchers.Store(folder, themeWatcher)
  168. done := make(chan bool)
  169. go func() {
  170. for {
  171. select {
  172. case event, ok := <-themeWatcher.Events:
  173. if !ok {
  174. return
  175. }
  176. //logging.LogInfof(event.String())
  177. if event.Op&fsnotify.Write == fsnotify.Write && (strings.HasSuffix(event.Name, "theme.css")) {
  178. var themeName string
  179. if themeName = isCurrentUseTheme(event.Name); "" == themeName {
  180. break
  181. }
  182. if strings.HasSuffix(event.Name, "theme.css") {
  183. util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
  184. "theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()),
  185. })
  186. break
  187. }
  188. }
  189. case err, ok := <-themeWatcher.Errors:
  190. if !ok {
  191. return
  192. }
  193. logging.LogErrorf("watch theme file failed: %s", err)
  194. }
  195. }
  196. }()
  197. //logging.LogInfof("add file watcher [%s]", folder)
  198. if err := themeWatcher.Add(folder); err != nil {
  199. logging.LogErrorf("add theme files watcher for folder [%s] failed: %s", folder, err)
  200. }
  201. <-done
  202. }
  203. func isCurrentUseTheme(themePath string) string {
  204. themeName := filepath.Base(filepath.Dir(themePath))
  205. if 0 == Conf.Appearance.Mode { // 明亮
  206. if Conf.Appearance.ThemeLight == themeName {
  207. return themeName
  208. }
  209. } else if 1 == Conf.Appearance.Mode { // 暗黑
  210. if Conf.Appearance.ThemeDark == themeName {
  211. return themeName
  212. }
  213. }
  214. return ""
  215. }