A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 294 lines 7.9 kB view raw
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 17package model 18 19import ( 20 "fmt" 21 "os" 22 "path/filepath" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/88250/gulu" 28 "github.com/fsnotify/fsnotify" 29 "github.com/siyuan-note/filelock" 30 "github.com/siyuan-note/logging" 31 "github.com/siyuan-note/siyuan/kernel/bazaar" 32 "github.com/siyuan-note/siyuan/kernel/conf" 33 "github.com/siyuan-note/siyuan/kernel/util" 34) 35 36func InitAppearance() { 37 util.SetBootDetails("Initializing appearance...") 38 if err := os.Mkdir(util.AppearancePath, 0755); err != nil && !os.IsExist(err) { 39 logging.LogErrorf("create appearance folder [%s] failed: %s", util.AppearancePath, err) 40 util.ReportFileSysFatalError(err) 41 return 42 } 43 44 unloadThemes() 45 from := filepath.Join(util.WorkingDir, "appearance") 46 if err := filelock.Copy(from, util.AppearancePath); err != nil { 47 logging.LogErrorf("copy appearance resources from [%s] to [%s] failed: %s", from, util.AppearancePath, err) 48 util.ReportFileSysFatalError(err) 49 return 50 } 51 loadThemes() 52 53 if !containTheme(Conf.Appearance.ThemeDark, Conf.Appearance.DarkThemes) { 54 Conf.Appearance.ThemeDark = "midnight" 55 Conf.Appearance.ThemeJS = false 56 } 57 if !containTheme(Conf.Appearance.ThemeLight, Conf.Appearance.LightThemes) { 58 Conf.Appearance.ThemeLight = "daylight" 59 Conf.Appearance.ThemeJS = false 60 } 61 62 loadIcons() 63 if !gulu.Str.Contains(Conf.Appearance.Icon, Conf.Appearance.Icons) { 64 Conf.Appearance.Icon = "material" 65 } 66 67 Conf.Save() 68 69 util.InitEmojiChars() 70} 71 72func containTheme(name string, themes []*conf.AppearanceTheme) bool { 73 for _, t := range themes { 74 if t.Name == name { 75 return true 76 } 77 } 78 return false 79} 80 81var themeWatchers = sync.Map{} // [string]*fsnotify.Watcher{} 82 83func closeThemeWatchers() { 84 themeWatchers.Range(func(key, value interface{}) bool { 85 if err := value.(*fsnotify.Watcher).Close(); err != nil { 86 logging.LogErrorf("close file watcher failed: %s", err) 87 } 88 return true 89 }) 90} 91 92func unloadThemes() { 93 if !util.IsPathRegularDirOrSymlinkDir(util.ThemesPath) { 94 return 95 } 96 97 themeDirs, err := os.ReadDir(util.ThemesPath) 98 if err != nil { 99 logging.LogErrorf("read appearance themes folder failed: %s", err) 100 return 101 } 102 103 for _, themeDir := range themeDirs { 104 if !util.IsDirRegularOrSymlink(themeDir) { 105 continue 106 } 107 unwatchTheme(filepath.Join(util.ThemesPath, themeDir.Name())) 108 } 109} 110 111func loadThemes() { 112 themeDirs, err := os.ReadDir(util.ThemesPath) 113 if err != nil { 114 logging.LogErrorf("read appearance themes folder failed: %s", err) 115 util.ReportFileSysFatalError(err) 116 return 117 } 118 119 Conf.Appearance.DarkThemes = nil 120 Conf.Appearance.LightThemes = nil 121 var daylightTheme, midnightTheme *conf.AppearanceTheme 122 for _, themeDir := range themeDirs { 123 if !util.IsDirRegularOrSymlink(themeDir) { 124 continue 125 } 126 name := themeDir.Name() 127 themeConf, parseErr := bazaar.ThemeJSON(name) 128 if nil != parseErr || nil == themeConf { 129 continue 130 } 131 132 modes := themeConf.Modes 133 for _, mode := range modes { 134 t := &conf.AppearanceTheme{Name: name} 135 if "zh_CN" == util.Lang { 136 if "midnight" == name { 137 t.Label = name + "(默认主题)" 138 } else if "daylight" == name { 139 t.Label = name + "(默认主题)" 140 } else { 141 if nil != themeConf.DisplayName && "" != themeConf.DisplayName.ZhCN && name != themeConf.DisplayName.ZhCN { 142 t.Label = themeConf.DisplayName.ZhCN + "(" + name + ")" 143 } else { 144 t.Label = name 145 } 146 } 147 } else { 148 if "midnight" == name { 149 t.Label = name + " (Default)" 150 } else if "daylight" == name { 151 t.Label = name + " (Default)" 152 } else { 153 t.Label = name 154 } 155 } 156 157 if "midnight" == name { 158 midnightTheme = t 159 continue 160 } else if "daylight" == name { 161 daylightTheme = t 162 continue 163 } 164 165 if "dark" == mode { 166 Conf.Appearance.DarkThemes = append(Conf.Appearance.DarkThemes, t) 167 } else if "light" == mode { 168 Conf.Appearance.LightThemes = append(Conf.Appearance.LightThemes, t) 169 } 170 } 171 172 if 0 == Conf.Appearance.Mode { 173 if Conf.Appearance.ThemeLight == name { 174 Conf.Appearance.ThemeVer = themeConf.Version 175 Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js")) 176 } 177 } else { 178 if Conf.Appearance.ThemeDark == name { 179 Conf.Appearance.ThemeVer = themeConf.Version 180 Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js")) 181 } 182 } 183 184 go watchTheme(filepath.Join(util.ThemesPath, name)) 185 } 186 187 Conf.Appearance.LightThemes = append([]*conf.AppearanceTheme{daylightTheme}, Conf.Appearance.LightThemes...) 188 Conf.Appearance.DarkThemes = append([]*conf.AppearanceTheme{midnightTheme}, Conf.Appearance.DarkThemes...) 189} 190 191func loadIcons() { 192 iconDirs, err := os.ReadDir(util.IconsPath) 193 if err != nil { 194 logging.LogErrorf("read appearance icons folder failed: %s", err) 195 util.ReportFileSysFatalError(err) 196 return 197 } 198 199 Conf.Appearance.Icons = nil 200 for _, iconDir := range iconDirs { 201 if !util.IsDirRegularOrSymlink(iconDir) { 202 continue 203 } 204 name := iconDir.Name() 205 iconConf, err := bazaar.IconJSON(name) 206 if err != nil || nil == iconConf { 207 continue 208 } 209 Conf.Appearance.Icons = append(Conf.Appearance.Icons, name) 210 if Conf.Appearance.Icon == name { 211 Conf.Appearance.IconVer = iconConf.Version 212 } 213 } 214} 215 216func ReloadIcon() { 217 loadIcons() 218} 219 220func unwatchTheme(folder string) { 221 val, _ := themeWatchers.Load(folder) 222 if nil != val { 223 themeWatcher := val.(*fsnotify.Watcher) 224 themeWatcher.Close() 225 } 226} 227 228func watchTheme(folder string) { 229 val, _ := themeWatchers.Load(folder) 230 var themeWatcher *fsnotify.Watcher 231 if nil != val { 232 themeWatcher = val.(*fsnotify.Watcher) 233 themeWatcher.Close() 234 } 235 236 var err error 237 if themeWatcher, err = fsnotify.NewWatcher(); err != nil { 238 logging.LogErrorf("add theme file watcher for folder [%s] failed: %s", folder, err) 239 return 240 } 241 themeWatchers.Store(folder, themeWatcher) 242 243 done := make(chan bool) 244 go func() { 245 for { 246 select { 247 case event, ok := <-themeWatcher.Events: 248 if !ok { 249 return 250 } 251 252 //logging.LogInfof(event.String()) 253 if event.Op&fsnotify.Write == fsnotify.Write && (strings.HasSuffix(event.Name, "theme.css")) { 254 var themeName string 255 if themeName = isCurrentUseTheme(event.Name); "" == themeName { 256 break 257 } 258 259 if strings.HasSuffix(event.Name, "theme.css") { 260 util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{ 261 "theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()), 262 }) 263 break 264 } 265 } 266 case err, ok := <-themeWatcher.Errors: 267 if !ok { 268 return 269 } 270 logging.LogErrorf("watch theme file failed: %s", err) 271 } 272 } 273 }() 274 275 //logging.LogInfof("add file watcher [%s]", folder) 276 if err := themeWatcher.Add(folder); err != nil { 277 logging.LogErrorf("add theme files watcher for folder [%s] failed: %s", folder, err) 278 } 279 <-done 280} 281 282func isCurrentUseTheme(themePath string) string { 283 themeName := filepath.Base(filepath.Dir(themePath)) 284 if 0 == Conf.Appearance.Mode { // 明亮 285 if Conf.Appearance.ThemeLight == themeName { 286 return themeName 287 } 288 } else if 1 == Conf.Appearance.Mode { // 暗黑 289 if Conf.Appearance.ThemeDark == themeName { 290 return themeName 291 } 292 } 293 return "" 294}