feat: Theme loading and metadata
commitc92f17c167Author: Minecon724 <minecon724@noreply.git.m724.eu> Date: Sat Sep 13 10:46:57 2025 +0000 Display everything in the UI commitfa533b8ba5Author: Minecon724 <minecon724@noreply.git.m724.eu> Date: Sat Sep 13 10:46:40 2025 +0000 Add theme loader commitadbd5af8d1Author: Minecon724 <minecon724@noreply.git.m724.eu> Date: Sat Sep 13 10:17:00 2025 +0000 Fix webpack commit933ad0b23aAuthor: Minecon724 <minecon724@noreply.git.m724.eu> Date: Sat Sep 13 10:16:51 2025 +0000 Add metadata to builtin themes commit1b8aa2fc35Author: Minecon724 <minecon724@noreply.git.m724.eu> Date: Sat Sep 13 10:16:38 2025 +0000 Remove accessibility themes
This commit is contained in:
		
					parent
					
						
							
								4ba0067056
							
						
					
				
			
			
				commit
				
					
						efe6cbaed5
					
				
			
		
					 21 changed files with 373 additions and 82 deletions
				
			
		| 
						 | 
					@ -16,6 +16,7 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_ "net/http/pprof" // Used for debugging if enabled and a web server is running
 | 
						_ "net/http/pprof" // Used for debugging if enabled and a web server is running
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"forgejo.org/models/theme"
 | 
				
			||||||
	"forgejo.org/modules/container"
 | 
						"forgejo.org/modules/container"
 | 
				
			||||||
	"forgejo.org/modules/graceful"
 | 
						"forgejo.org/modules/graceful"
 | 
				
			||||||
	"forgejo.org/modules/log"
 | 
						"forgejo.org/modules/log"
 | 
				
			||||||
| 
						 | 
					@ -217,6 +218,13 @@ func serveInstalled(_ context.Context, ctx *cli.Command) error {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Load themes
 | 
				
			||||||
 | 
						themes, tlerr := theme.GetThemeIds()
 | 
				
			||||||
 | 
						if (tlerr != nil) {
 | 
				
			||||||
 | 
							log.Error("Failed to load themes: ", tlerr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						setting.UI.Themes = themes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set up Chi routes
 | 
						// Set up Chi routes
 | 
				
			||||||
	webRoutes := routers.NormalRoutes()
 | 
						webRoutes := routers.NormalRoutes()
 | 
				
			||||||
	err := listen(webRoutes, true)
 | 
						err := listen(webRoutes, true)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										107
									
								
								models/theme/theme_loader.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								models/theme/theme_loader.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,107 @@
 | 
				
			||||||
 | 
					package theme
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"maps"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"forgejo.org/modules/assetfs"
 | 
				
			||||||
 | 
						"forgejo.org/modules/log"
 | 
				
			||||||
 | 
						"forgejo.org/modules/public"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/sync/singleflight"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						themeFilenamePrefix = "theme-"
 | 
				
			||||||
 | 
						themeFilenameSuffix = ".css"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						group        singleflight.Group
 | 
				
			||||||
 | 
						assetFs      *assetfs.LayeredFS
 | 
				
			||||||
 | 
						loaded       bool
 | 
				
			||||||
 | 
						loadedThemes map[string]Theme // this static variable feels suspicious
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetThemeIds() ([]string, error) {
 | 
				
			||||||
 | 
						themes, err := GetThemes()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return slices.Collect(maps.Keys(themes)), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetThemes gets the installed Themes.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// It doesn't install them to settings.UI.Themes.
 | 
				
			||||||
 | 
					func GetThemes() (map[string]Theme, error) {
 | 
				
			||||||
 | 
						if loaded {
 | 
				
			||||||
 | 
							return loadedThemes, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if assetFs == nil {
 | 
				
			||||||
 | 
							assetFs = public.AssetFS()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err, _ := group.Do("load-themes", func() (any, error) {
 | 
				
			||||||
 | 
							_themes, err := loadThemes(assetFs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Info("Loaded %d themes", len(_themes))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								// Retry on error TODO might want to improve that
 | 
				
			||||||
 | 
								group.Forget("load-themes")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								loadedThemes = _themes
 | 
				
			||||||
 | 
								loaded = true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return loadedThemes, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// loadThemes scans for and loads themes from an asset filesystem.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Returns: a map of theme ID: theme metadata.
 | 
				
			||||||
 | 
					func loadThemes(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, themeFilenamePrefix) && strings.HasSuffix(entry, themeFilenameSuffix)) {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							id := entry[len(themeFilenamePrefix) : len(entry)-len(themeFilenameSuffix)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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", entry, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							themes[id] = theme
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Debug("Found theme: %s (%s)", theme.Name, id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(themes) == 0 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("no themes found")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return themes, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										108
									
								
								models/theme/theme_meta.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								models/theme/theme_meta.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,108 @@
 | 
				
			||||||
 | 
					package theme
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"forgejo.org/modules/assetfs"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// The Theme struct represents the metadata of a theme. Most fields are allowed to be null / blank.
 | 
				
			||||||
 | 
					type Theme struct {
 | 
				
			||||||
 | 
						// ID is the theme id, e.g., forgejo-dark
 | 
				
			||||||
 | 
						ID 			  	string
 | 
				
			||||||
 | 
						// Name is the theme's readable name, e.g., Forgejo. Note that the scheme is excluded in favor of the Scheme property.
 | 
				
			||||||
 | 
						Name		   	string
 | 
				
			||||||
 | 
						// Family is the theme's family, e.g. Catppuccin.
 | 
				
			||||||
 | 
						Family      string
 | 
				
			||||||
 | 
						// Author is the theme's author. It may be an invididual, a group, or an entity.
 | 
				
			||||||
 | 
						Author      string
 | 
				
			||||||
 | 
						// Url is the URL to the theme's website, e.g. https://forgejo.org.
 | 
				
			||||||
 | 
						Url         string
 | 
				
			||||||
 | 
						// Description is the theme's short description.
 | 
				
			||||||
 | 
						Description string
 | 
				
			||||||
 | 
						// Scheme is the theme's color scheme. It must be either: dark, light, or auto.
 | 
				
			||||||
 | 
						Scheme      string
 | 
				
			||||||
 | 
						// Recommended means the theme is guaranteed to be bug-free, usually a first-party theme.
 | 
				
			||||||
 | 
						Recommended bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// loadThemeMeta loads theme metadata from a file in a layered FS.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Theme ID is expected to be prefilled. The metadata will be assigned to the passed `theme` object.
 | 
				
			||||||
 | 
					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 := scanThemeForKeyValueMetaMap(file, theme.ID) // ID is expected to be prefilled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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.Recommended = kv["recommended"] == "yes"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// scanThemeForKeyValueMetaMap finds raw key-value metadata in an open file.
 | 
				
			||||||
 | 
					func scanThemeForKeyValueMetaMap(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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								models/theme/theme_name.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								models/theme/theme_name.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					package theme
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getFriendlyThemeName converts an raw theme name to a friendly, readable name.
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -812,6 +812,10 @@ manage_themes = Default theme
 | 
				
			||||||
manage_openid = OpenID addresses
 | 
					manage_openid = OpenID addresses
 | 
				
			||||||
email_desc = Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations.
 | 
					email_desc = Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations.
 | 
				
			||||||
theme_desc = This theme will be used for the web interface when you are logged in.
 | 
					theme_desc = This theme will be used for the web interface when you are logged in.
 | 
				
			||||||
 | 
					theme_recommended = Recommended
 | 
				
			||||||
 | 
					theme_light = Light
 | 
				
			||||||
 | 
					theme_dark = Dark
 | 
				
			||||||
 | 
					theme_auto = Auto (per system)
 | 
				
			||||||
primary = Primary
 | 
					primary = Primary
 | 
				
			||||||
activated = Activated
 | 
					activated = Activated
 | 
				
			||||||
requires_activation = Requires activation
 | 
					requires_activation = Requires activation
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ import (
 | 
				
			||||||
	"forgejo.org/models/db"
 | 
						"forgejo.org/models/db"
 | 
				
			||||||
	"forgejo.org/models/organization"
 | 
						"forgejo.org/models/organization"
 | 
				
			||||||
	repo_model "forgejo.org/models/repo"
 | 
						repo_model "forgejo.org/models/repo"
 | 
				
			||||||
 | 
						"forgejo.org/models/theme"
 | 
				
			||||||
	user_model "forgejo.org/models/user"
 | 
						user_model "forgejo.org/models/user"
 | 
				
			||||||
	"forgejo.org/modules/base"
 | 
						"forgejo.org/modules/base"
 | 
				
			||||||
	"forgejo.org/modules/log"
 | 
						"forgejo.org/modules/log"
 | 
				
			||||||
| 
						 | 
					@ -335,15 +336,15 @@ func Repos(ctx *context.Context) {
 | 
				
			||||||
func Appearance(ctx *context.Context) {
 | 
					func Appearance(ctx *context.Context) {
 | 
				
			||||||
	ctx.Data["Title"] = ctx.Tr("settings.appearance")
 | 
						ctx.Data["Title"] = ctx.Tr("settings.appearance")
 | 
				
			||||||
	ctx.Data["PageIsSettingsAppearance"] = true
 | 
						ctx.Data["PageIsSettingsAppearance"] = true
 | 
				
			||||||
	ctx.Data["AllThemes"] = setting.UI.Themes
 | 
					
 | 
				
			||||||
	ctx.Data["ThemeName"] = func(themeName string) string {
 | 
						themes, err := theme.GetThemes()
 | 
				
			||||||
		fullThemeName := "themes.names." + themeName
 | 
						if err != nil {
 | 
				
			||||||
		if ctx.Locale.HasKey(fullThemeName) {
 | 
							ctx.ServerError("Failed to load themes", err)
 | 
				
			||||||
			return ctx.Locale.TrString(fullThemeName)
 | 
							return
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return themeName
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Data["AllThemes"] = themes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var hiddenCommentTypes *big.Int
 | 
						var hiddenCommentTypes *big.Int
 | 
				
			||||||
	val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
 | 
						val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,37 +7,64 @@
 | 
				
			||||||
		</h4>
 | 
							</h4>
 | 
				
			||||||
		<div class="ui attached segment">
 | 
							<div class="ui attached segment">
 | 
				
			||||||
			<div class="ui email list">
 | 
								<div class="ui email list">
 | 
				
			||||||
				<div class="item">
 | 
									{{$theme := index .AllThemes $.SignedUser.Theme}}
 | 
				
			||||||
					{{ctx.Locale.Tr "settings.theme_desc"}}
 | 
									{{if $theme.Author}}
 | 
				
			||||||
				</div>
 | 
										<div class="item">
 | 
				
			||||||
 | 
											<p>
 | 
				
			||||||
			<form class="ui form" action="{{.Link}}/theme" method="post">
 | 
												<strong>{{$theme.Name}}</strong>
 | 
				
			||||||
				{{.CsrfTokenHtml}}
 | 
												by <em><a href="{{$theme.Url}}">{{$theme.Author}}</a></em> {{if $theme.Recommended}}<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_recommended"}}">{{svg "octicon-verified" 16 "dropdown icon"}}</span>{{end}}
 | 
				
			||||||
					<div class="field">
 | 
											</p>
 | 
				
			||||||
						<label for="ui">{{ctx.Locale.Tr "settings.ui"}}</label>
 | 
											{{if $theme.Description}}
 | 
				
			||||||
						<div class="ui selection dropdown" id="ui">
 | 
												<p><em>"{{$theme.Description}}"</em></p>
 | 
				
			||||||
							<input name="theme" type="hidden" value="{{.SignedUser.Theme}}">
 | 
											{{end}}
 | 
				
			||||||
							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
					 | 
				
			||||||
							<div class="text">
 | 
					 | 
				
			||||||
								{{- range $i,$a := .AllThemes -}}
 | 
					 | 
				
			||||||
									{{if eq $.SignedUser.Theme $a}}{{call $.ThemeName $a}}{{end}}
 | 
					 | 
				
			||||||
								{{- end -}}
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							<div class="menu">
 | 
					 | 
				
			||||||
							{{range $i,$a := .AllThemes}}
 | 
					 | 
				
			||||||
								<div class="item{{if eq $.SignedUser.Theme $a}} active selected{{end}}" data-value="{{$a}}">
 | 
					 | 
				
			||||||
									{{call $.ThemeName $a}}
 | 
					 | 
				
			||||||
								</div>
 | 
					 | 
				
			||||||
							{{end}}
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
						</div>
 | 
					 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<div class="field">
 | 
										<div class="item tw-mb-auto">
 | 
				
			||||||
					<button class="ui primary button">{{ctx.Locale.Tr "settings.update_theme"}}</button>
 | 
											{{ctx.Locale.Tr "settings.theme_desc"}}
 | 
				
			||||||
				</div>
 | 
										</div>
 | 
				
			||||||
			</form>
 | 
									{{else}}
 | 
				
			||||||
 | 
										<div class="item tw-mb-4">
 | 
				
			||||||
 | 
											{{ctx.Locale.Tr "settings.theme_desc"}}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<form class="ui form" action="{{.Link}}/theme" method="post">
 | 
				
			||||||
 | 
										{{.CsrfTokenHtml}}
 | 
				
			||||||
 | 
											<div class="field">
 | 
				
			||||||
 | 
												<label for="ui">{{ctx.Locale.Tr "settings.ui"}}</label>
 | 
				
			||||||
 | 
												<div class="ui selection dropdown" id="ui">
 | 
				
			||||||
 | 
													<input name="theme" type="hidden" value="{{.SignedUser.Theme}}">
 | 
				
			||||||
 | 
													{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 | 
				
			||||||
 | 
													<div class="text">
 | 
				
			||||||
 | 
														{{- range $i,$a := .AllThemes -}}
 | 
				
			||||||
 | 
															{{if eq $.SignedUser.Theme $i}}{{$a.Name}}{{end}}
 | 
				
			||||||
 | 
														{{- end -}}
 | 
				
			||||||
 | 
													</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
													<div class="menu">
 | 
				
			||||||
 | 
													{{range $i,$a := .AllThemes}}
 | 
				
			||||||
 | 
														<div class="item{{if eq $.SignedUser.Theme $i}} active selected{{end}} tw-flex" data-value="{{$i}}">
 | 
				
			||||||
 | 
															{{$a.Name}}
 | 
				
			||||||
 | 
															{{if $a.Recommended}}
 | 
				
			||||||
 | 
																<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_recommended"}}">{{svg "octicon-verified" 16 "dropdown icon"}}</span>
 | 
				
			||||||
 | 
															{{end}}
 | 
				
			||||||
 | 
															{{if eq $a.Scheme "auto"}}
 | 
				
			||||||
 | 
																<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_auto"}}">{{svg "octicon-sync" 16 "dropdown icon"}}</span>
 | 
				
			||||||
 | 
															{{else if eq $a.Scheme "dark"}}
 | 
				
			||||||
 | 
																<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_dark"}}">{{svg "octicon-moon" 16 "dropdown icon"}}</span>
 | 
				
			||||||
 | 
															{{else if eq $a.Scheme "light"}}
 | 
				
			||||||
 | 
																<span data-tooltip-content="{{ctx.Locale.Tr "settings.theme_light"}}">{{svg "octicon-sun" 16 "dropdown icon"}}</span>
 | 
				
			||||||
 | 
															{{end}}
 | 
				
			||||||
 | 
														</div>
 | 
				
			||||||
 | 
													{{end}}
 | 
				
			||||||
 | 
													</div>
 | 
				
			||||||
 | 
												</div>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<div class="field">
 | 
				
			||||||
 | 
											<button class="ui primary button">{{ctx.Locale.Tr "settings.update_theme"}}</button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</form>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,13 @@
 | 
				
			||||||
#!/bin/bash
 | 
					#!/bin/bash
 | 
				
			||||||
set -euo pipefail
 | 
					set -euo pipefail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
make --no-print-directory watch-frontend &
 | 
					make --no-print-directory watch-frontend | tee >(
 | 
				
			||||||
make --no-print-directory watch-backend &
 | 
					  awk -v pattern="webpack" '
 | 
				
			||||||
 | 
					    $0 ~ pattern {
 | 
				
			||||||
 | 
					      system("make --no-print-directory watch-backend &")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  '
 | 
				
			||||||
 | 
					) &
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trap 'kill $(jobs -p)' EXIT
 | 
					trap 'kill $(jobs -p)' EXIT
 | 
				
			||||||
wait
 | 
					wait
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,2 +0,0 @@
 | 
				
			||||||
@import "theme-forgejo-light-deuteranopia-protanopia.css";
 | 
					 | 
				
			||||||
@import "theme-forgejo-dark-deuteranopia-protanopia.css" (prefers-color-scheme: dark);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,2 +0,0 @@
 | 
				
			||||||
@import "theme-forgejo-light-tritanopia.css";
 | 
					 | 
				
			||||||
@import "theme-forgejo-dark-tritanopia.css" (prefers-color-scheme: dark);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,2 +1,12 @@
 | 
				
			||||||
 | 
					/* theme forgejo-auto
 | 
				
			||||||
 | 
					    Name Forgejo
 | 
				
			||||||
 | 
					    Family Forgejo
 | 
				
			||||||
 | 
					    Author Forgejo authors
 | 
				
			||||||
 | 
					    Url https://forgejo.org/
 | 
				
			||||||
 | 
					    Description The fresh look of freedom.
 | 
				
			||||||
 | 
					    Scheme Auto
 | 
				
			||||||
 | 
					    Recommended
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "theme-forgejo-light.css";
 | 
					@import "theme-forgejo-light.css";
 | 
				
			||||||
@import "theme-forgejo-dark.css" (prefers-color-scheme: dark);
 | 
					@import "theme-forgejo-dark.css" (prefers-color-scheme: dark);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +0,0 @@
 | 
				
			||||||
@import "./theme-forgejo-dark.css";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:root {
 | 
					 | 
				
			||||||
  /* removed rows/words: use red colors from vanilla forgejo-dark */
 | 
					 | 
				
			||||||
  --color-diff-added-word-bg: #214d88;
 | 
					 | 
				
			||||||
  --color-diff-added-row-border: #214d88;
 | 
					 | 
				
			||||||
  --color-diff-added-row-bg: #18184f;
 | 
					 | 
				
			||||||
  --color-code-bg: #0d1117;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,9 +0,0 @@
 | 
				
			||||||
@import "./theme-forgejo-dark.css";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:root {
 | 
					 | 
				
			||||||
  /* removed rows/words: use red colors from vanilla forgejo-dark */
 | 
					 | 
				
			||||||
  --color-diff-added-word-bg: #214d88;
 | 
					 | 
				
			||||||
  --color-diff-added-row-border: #214d88;
 | 
					 | 
				
			||||||
  --color-diff-added-row-bg: #152846;
 | 
					 | 
				
			||||||
  --color-code-bg: #0d1117;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,13 @@
 | 
				
			||||||
 | 
					/* theme forgejo-dark
 | 
				
			||||||
 | 
					    Name Forgejo
 | 
				
			||||||
 | 
					    Family Forgejo
 | 
				
			||||||
 | 
					    Author Forgejo authors
 | 
				
			||||||
 | 
					    Url https://forgejo.org/
 | 
				
			||||||
 | 
					    Description The fresh look of freedom.
 | 
				
			||||||
 | 
					    Scheme Dark
 | 
				
			||||||
 | 
					    Recommended
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "../chroma/dark.css";
 | 
					@import "../chroma/dark.css";
 | 
				
			||||||
@import "../codemirror/dark.css";
 | 
					@import "../codemirror/dark.css";
 | 
				
			||||||
@import "../markup/dark.css";
 | 
					@import "../markup/dark.css";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +0,0 @@
 | 
				
			||||||
@import "./theme-forgejo-light.css";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:root {
 | 
					 | 
				
			||||||
  --color-diff-removed-word-bg: #c8c850;
 | 
					 | 
				
			||||||
  --color-diff-removed-row-border: #c8c850;
 | 
					 | 
				
			||||||
  --color-diff-removed-row-bg: #ffecc4;
 | 
					 | 
				
			||||||
  --color-diff-added-word-bg: #b8c0ff;
 | 
					 | 
				
			||||||
  --color-diff-added-row-border: #b8c0ff;
 | 
					 | 
				
			||||||
  --color-diff-added-row-bg: #e0e0ff;
 | 
					 | 
				
			||||||
  --color-code-bg: #ffffff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,11 +0,0 @@
 | 
				
			||||||
@import "./theme-forgejo-light.css";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:root {
 | 
					 | 
				
			||||||
  --color-diff-removed-word-bg: #ffb8c0;
 | 
					 | 
				
			||||||
  --color-diff-removed-row-border: #ffb8c0;
 | 
					 | 
				
			||||||
  --color-diff-removed-row-bg: #ffd8d8;
 | 
					 | 
				
			||||||
  --color-diff-added-word-bg: #b8c0ff;
 | 
					 | 
				
			||||||
  --color-diff-added-row-border: #b8c0ff;
 | 
					 | 
				
			||||||
  --color-diff-added-row-bg: #c0e8ff;
 | 
					 | 
				
			||||||
  --color-code-bg: #ffffff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,13 @@
 | 
				
			||||||
 | 
					/* theme forgejo-light
 | 
				
			||||||
 | 
					    Name Forgejo
 | 
				
			||||||
 | 
					    Family Forgejo
 | 
				
			||||||
 | 
					    Author Forgejo authors
 | 
				
			||||||
 | 
					    Url https://forgejo.org/
 | 
				
			||||||
 | 
					    Description The fresh look of freedom.
 | 
				
			||||||
 | 
					    Scheme Light
 | 
				
			||||||
 | 
					    Recommended
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "../chroma/light.css";
 | 
					@import "../chroma/light.css";
 | 
				
			||||||
@import "../codemirror/light.css";
 | 
					@import "../codemirror/light.css";
 | 
				
			||||||
@import "../markup/light.css";
 | 
					@import "../markup/light.css";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,2 +1,12 @@
 | 
				
			||||||
 | 
					/* theme gitea-auto
 | 
				
			||||||
 | 
					    Name Gitea
 | 
				
			||||||
 | 
					    Family Gitea
 | 
				
			||||||
 | 
					    Author The Gitea Authors
 | 
				
			||||||
 | 
					    Url https://github.com/go-gitea/gitea
 | 
				
			||||||
 | 
					    Description The OG style.
 | 
				
			||||||
 | 
					    Scheme Auto
 | 
				
			||||||
 | 
					    Recommended
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "./theme-gitea-light.css" (prefers-color-scheme: light);
 | 
					@import "./theme-gitea-light.css" (prefers-color-scheme: light);
 | 
				
			||||||
@import "./theme-gitea-dark.css" (prefers-color-scheme: dark);
 | 
					@import "./theme-gitea-dark.css" (prefers-color-scheme: dark);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,13 @@
 | 
				
			||||||
 | 
					/* theme gitea-dark
 | 
				
			||||||
 | 
					    Name Gitea
 | 
				
			||||||
 | 
					    Family Gitea
 | 
				
			||||||
 | 
					    Author The Gitea Authors
 | 
				
			||||||
 | 
					    Url https://github.com/go-gitea/gitea
 | 
				
			||||||
 | 
					    Description The OG style.
 | 
				
			||||||
 | 
					    Scheme Dark
 | 
				
			||||||
 | 
					    Recommended
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "../chroma/dark.css";
 | 
					@import "../chroma/dark.css";
 | 
				
			||||||
@import "../codemirror/dark.css";
 | 
					@import "../codemirror/dark.css";
 | 
				
			||||||
@import "../markup/dark.css";
 | 
					@import "../markup/dark.css";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,13 @@
 | 
				
			||||||
 | 
					/* theme gitea-light
 | 
				
			||||||
 | 
					    Name Gitea
 | 
				
			||||||
 | 
					    Family Gitea
 | 
				
			||||||
 | 
					    Author The Gitea Authors
 | 
				
			||||||
 | 
					    Url https://github.com/go-gitea/gitea
 | 
				
			||||||
 | 
					    Description The OG style.
 | 
				
			||||||
 | 
					    Scheme Light
 | 
				
			||||||
 | 
					    Recommended
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "../chroma/light.css";
 | 
					@import "../chroma/light.css";
 | 
				
			||||||
@import "../codemirror/light.css";
 | 
					@import "../codemirror/light.css";
 | 
				
			||||||
@import "../markup/light.css";
 | 
					@import "../markup/light.css";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -157,6 +157,7 @@ export default {
 | 
				
			||||||
        minify: true,
 | 
					        minify: true,
 | 
				
			||||||
        css: true,
 | 
					        css: true,
 | 
				
			||||||
        legalComments: 'none',
 | 
					        legalComments: 'none',
 | 
				
			||||||
 | 
					        exclude: [new RegExp(`css\\/(${Object.keys(themes).join('|')})\\.css$`)],
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    splitChunks: {
 | 
					    splitChunks: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue