Some checks are pending
/ release (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Display is rough right now but better than nothing. Icons especially to be improved.
189 lines
3.6 KiB
Go
189 lines
3.6 KiB
Go
package theme
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
|
|
"forgejo.org/modules/assetfs"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/public"
|
|
"forgejo.org/modules/setting"
|
|
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
var (
|
|
group singleflight.Group
|
|
assetFs *assetfs.LayeredFS
|
|
loaded bool
|
|
themes map[string]Theme
|
|
)
|
|
|
|
type Theme struct {
|
|
ID string
|
|
Name string
|
|
Family string
|
|
Author string
|
|
Url string
|
|
Description string
|
|
Scheme string
|
|
Verified bool
|
|
}
|
|
|
|
// GetThemes gets installed Themes
|
|
//
|
|
// Note that you can't just run this during init, because webpack mightn't have completed yet.
|
|
// Hence, we're loading on first demand.
|
|
func GetThemes() (map[string]Theme, error) {
|
|
if loaded {
|
|
return themes, nil
|
|
}
|
|
|
|
if assetFs == nil {
|
|
assetFs = public.AssetFS()
|
|
}
|
|
|
|
_, err, _ := group.Do("load-themes", func() (any, error) {
|
|
_themes, err := loadThemesInner(assetFs)
|
|
|
|
log.Info("Loaded %d themes", len(_themes))
|
|
|
|
if err != nil {
|
|
group.Forget("load-themes")
|
|
} else {
|
|
setting.UI.Themes = slices.Collect(maps.Keys(_themes))
|
|
themes = _themes
|
|
loaded = true
|
|
}
|
|
|
|
return nil, err
|
|
})
|
|
|
|
return themes, err
|
|
}
|
|
|
|
func loadThemesInner(assetFs *assetfs.LayeredFS) (map[string]Theme, error) {
|
|
entries, err := assetFs.ListFiles("assets/css")
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
themes := make(map[string]Theme)
|
|
|
|
for _, entry := range entries {
|
|
if !(strings.HasPrefix(entry, "theme-") && strings.HasSuffix(entry, ".css")) {
|
|
continue
|
|
}
|
|
|
|
id := entry[6 : len(entry)-4]
|
|
theme := Theme{
|
|
ID: id,
|
|
Name: getFriendlyThemeName(id),
|
|
}
|
|
|
|
if err := loadThemeMeta(assetFs, entry, &theme); err != nil {
|
|
log.Warn("Failed to load meta of theme %s: %s", id, err)
|
|
}
|
|
|
|
themes[id] = theme
|
|
|
|
log.Debug("Found theme: %s (%s)", theme.Name, theme.ID)
|
|
}
|
|
|
|
if len(themes) == 0 {
|
|
return nil, fmt.Errorf("no themes found")
|
|
}
|
|
|
|
return themes, nil
|
|
}
|
|
|
|
// getFriendlyThemeName converts an raw theme name (forgejo-dark) to a friendly name (Forgejo Dark)
|
|
//
|
|
// Example: forgejo-dark -> Forgejo Dark, catppuccin-maroon-auto -> Catppuccin Maroon Auto
|
|
func getFriendlyThemeName(themeName string) string {
|
|
themeName = strings.ReplaceAll(themeName, "-", " ")
|
|
|
|
themeName = strings.ToLower(themeName)
|
|
themeName = strings.Title(themeName)
|
|
|
|
return themeName
|
|
}
|
|
|
|
func loadThemeMeta(assetFs *assetfs.LayeredFS, filename string, theme *Theme) error {
|
|
file, err := assetFs.Open("assets/css/" + filename)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
kv, err := scanTheme(file, filename[6:len(filename)-4])
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error scanning theme for meta: %w", err)
|
|
}
|
|
|
|
theme.Name = kv["name"]
|
|
theme.Family = kv["family"]
|
|
theme.Author = kv["author"]
|
|
theme.Url = kv["url"]
|
|
theme.Description = kv["description"]
|
|
theme.Scheme = strings.ToLower(kv["scheme"])
|
|
theme.Verified = kv["verified"] == "yes"
|
|
|
|
return nil
|
|
}
|
|
|
|
func scanTheme(file io.Reader, id string) (map[string]string, error) {
|
|
var kv = make(map[string]string)
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
ok := false
|
|
|
|
// webpack adds headers
|
|
for scanner.Scan() {
|
|
if strings.TrimSpace(scanner.Text()) == "/* theme "+id {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !ok {
|
|
return nil, errors.New("theme has no meta")
|
|
}
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
parts := strings.Fields(line)
|
|
|
|
if len(parts) == 0 {
|
|
continue
|
|
}
|
|
|
|
key := strings.ToLower(parts[0])
|
|
|
|
if key == "*/" {
|
|
break
|
|
}
|
|
|
|
if len(parts) == 1 {
|
|
kv[key] = "yes"
|
|
} else {
|
|
kv[key] = strings.Join(parts[1:], " ")
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("scanner error: %w", err)
|
|
}
|
|
|
|
return kv, nil
|
|
}
|