theme.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // SiYuan - Build Your Eternal Digital Garden
  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 bazaar
  17. import (
  18. "errors"
  19. "os"
  20. "path/filepath"
  21. "sort"
  22. "strings"
  23. "sync"
  24. "github.com/88250/gulu"
  25. "github.com/dustin/go-humanize"
  26. ants "github.com/panjf2000/ants/v2"
  27. "github.com/siyuan-note/httpclient"
  28. "github.com/siyuan-note/logging"
  29. "github.com/siyuan-note/siyuan/kernel/util"
  30. )
  31. type Theme struct {
  32. Author string `json:"author"`
  33. URL string `json:"url"`
  34. Version string `json:"version"`
  35. Modes []string `json:"modes"`
  36. Name string `json:"name"`
  37. RepoURL string `json:"repoURL"`
  38. RepoHash string `json:"repoHash"`
  39. PreviewURL string `json:"previewURL"`
  40. PreviewURLThumb string `json:"previewURLThumb"`
  41. README string `json:"readme"`
  42. Installed bool `json:"installed"`
  43. Outdated bool `json:"outdated"`
  44. Current bool `json:"current"`
  45. Updated string `json:"updated"`
  46. Stars int `json:"stars"`
  47. OpenIssues int `json:"openIssues"`
  48. Size int64 `json:"size"`
  49. HSize string `json:"hSize"`
  50. HUpdated string `json:"hUpdated"`
  51. Downloads int `json:"downloads"`
  52. }
  53. func Themes() (ret []*Theme) {
  54. ret = []*Theme{}
  55. result, err := util.GetRhyResult(false)
  56. if nil != err {
  57. return
  58. }
  59. bazaarIndex := getBazaarIndex()
  60. bazaarHash := result["bazaar"].(string)
  61. result = map[string]interface{}{}
  62. request := httpclient.NewBrowserRequest()
  63. u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/themes.json"
  64. resp, reqErr := request.SetResult(&result).Get(u)
  65. if nil != reqErr {
  66. logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
  67. return
  68. }
  69. if 200 != resp.StatusCode {
  70. logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
  71. return
  72. }
  73. repos := result["repos"].([]interface{})
  74. waitGroup := &sync.WaitGroup{}
  75. lock := &sync.Mutex{}
  76. p, _ := ants.NewPoolWithFunc(8, func(arg interface{}) {
  77. defer waitGroup.Done()
  78. repo := arg.(map[string]interface{})
  79. repoURL := repo["url"].(string)
  80. theme := &Theme{}
  81. innerU := util.BazaarOSSServer + "/package/" + repoURL + "/theme.json"
  82. innerResp, innerErr := httpclient.NewBrowserRequest().SetResult(theme).Get(innerU)
  83. if nil != innerErr {
  84. logging.LogErrorf("get bazaar package [%s] failed: %s", innerU, innerErr)
  85. return
  86. }
  87. if 200 != innerResp.StatusCode {
  88. logging.LogErrorf("get bazaar package [%s] failed: %d", innerU, resp.StatusCode)
  89. return
  90. }
  91. repoURLHash := strings.Split(repoURL, "@")
  92. theme.RepoURL = "https://github.com/" + repoURLHash[0]
  93. theme.RepoHash = repoURLHash[1]
  94. theme.PreviewURL = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageslim"
  95. theme.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageView2/2/w/436/h/232"
  96. theme.Updated = repo["updated"].(string)
  97. theme.Stars = int(repo["stars"].(float64))
  98. theme.OpenIssues = int(repo["openIssues"].(float64))
  99. theme.Size = int64(repo["size"].(float64))
  100. theme.HSize = humanize.Bytes(uint64(theme.Size))
  101. theme.HUpdated = formatUpdated(theme.Updated)
  102. pkg := bazaarIndex[strings.Split(repoURL, "@")[0]]
  103. if nil != pkg {
  104. theme.Downloads = pkg.Downloads
  105. }
  106. lock.Lock()
  107. ret = append(ret, theme)
  108. lock.Unlock()
  109. })
  110. for _, repo := range repos {
  111. waitGroup.Add(1)
  112. p.Invoke(repo)
  113. }
  114. waitGroup.Wait()
  115. p.Release()
  116. sort.Slice(ret, func(i, j int) bool { return ret[i].Updated > ret[j].Updated })
  117. return
  118. }
  119. func InstalledThemes() (ret []*Theme) {
  120. dir, err := os.Open(util.ThemesPath)
  121. if nil != err {
  122. logging.LogWarnf("open appearance themes folder [%s] failed: %s", util.ThemesPath, err)
  123. return
  124. }
  125. themeDirs, err := dir.Readdir(-1)
  126. if nil != err {
  127. logging.LogWarnf("read appearance themes folder failed: %s", err)
  128. return
  129. }
  130. dir.Close()
  131. for _, themeDir := range themeDirs {
  132. if !themeDir.IsDir() {
  133. continue
  134. }
  135. dirName := themeDir.Name()
  136. if isBuiltInTheme(dirName) {
  137. continue
  138. }
  139. themeConf, parseErr := ThemeJSON(dirName)
  140. if nil != parseErr || nil == themeConf {
  141. continue
  142. }
  143. theme := &Theme{}
  144. theme.Name = themeConf["name"].(string)
  145. theme.Author = themeConf["author"].(string)
  146. theme.URL = themeConf["url"].(string)
  147. theme.Version = themeConf["version"].(string)
  148. theme.Modes = make([]string, 0, len(themeConf["modes"].([]interface{})))
  149. theme.RepoURL = theme.URL
  150. theme.PreviewURL = "/appearance/themes/" + dirName + "/preview.png"
  151. theme.PreviewURLThumb = "/appearance/themes/" + dirName + "/preview.png"
  152. theme.Updated = themeDir.ModTime().Format("2006-01-02 15:04:05")
  153. theme.Size = themeDir.Size()
  154. theme.HSize = humanize.Bytes(uint64(theme.Size))
  155. theme.HUpdated = formatUpdated(theme.Updated)
  156. readme, readErr := os.ReadFile(filepath.Join(util.ThemesPath, dirName, "README.md"))
  157. if nil != readErr {
  158. logging.LogWarnf("read install theme README.md failed: %s", readErr)
  159. continue
  160. }
  161. theme.README = gulu.Str.FromBytes(readme)
  162. ret = append(ret, theme)
  163. }
  164. return
  165. }
  166. func isBuiltInTheme(dirName string) bool {
  167. return "daylight" == dirName || "midnight" == dirName
  168. }
  169. func InstallTheme(repoURL, repoHash, installPath string, systemID string) error {
  170. repoURLHash := repoURL + "@" + repoHash
  171. data, err := downloadPackage(repoURLHash, true, systemID)
  172. if nil != err {
  173. return err
  174. }
  175. return installPackage(data, installPath)
  176. }
  177. func UninstallTheme(installPath string) error {
  178. if err := os.RemoveAll(installPath); nil != err {
  179. logging.LogErrorf("remove theme [%s] failed: %s", installPath, err)
  180. return errors.New("remove community theme failed")
  181. }
  182. //logging.Logger.Infof("uninstalled theme [%s]", installPath)
  183. return nil
  184. }