This commit is contained in:
wxiaoguang 2025-10-26 14:58:41 +01:00 committed by GitHub
commit 4f9237c0ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 250 additions and 59 deletions

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
// SessionConfig defines Session settings
@ -49,10 +50,8 @@ func loadSessionFrom(rootCfg ConfigProvider) {
checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
}
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
SessionConfig.CookiePath = AppSubURL
if SessionConfig.CookiePath == "" {
SessionConfig.CookiePath = "/"
}
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
SessionConfig.CookiePath = util.IfZero(AppSubURL, "/")
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)

View File

@ -58,6 +58,9 @@ func MockIcon(icon string) func() {
// RenderHTML renders icons - arguments icon name (string), size (int), class (string)
func RenderHTML(icon string, others ...any) template.HTML {
if icon == "" {
return ""
}
size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...)
if svgStr, ok := svgIcons[icon]; ok {
// the code is somewhat hacky, but it just works, because the SVG contents are all normalized

View File

@ -12,7 +12,6 @@ import (
"strings"
"time"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/markup"
@ -21,7 +20,6 @@ import (
"code.gitea.io/gitea/modules/templates/eval"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff"
"code.gitea.io/gitea/services/webtheme"
)
// NewFuncMap returns functions for injecting to templates
@ -130,7 +128,6 @@ func NewFuncMap() template.FuncMap {
"DisableWebhooks": func() bool {
return setting.DisableWebhooks
},
"UserThemeName": userThemeName,
"NotificationSettings": func() map[string]any {
return map[string]any{
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
@ -217,16 +214,6 @@ func evalTokens(tokens ...any) (any, error) {
return n.Value, err
}
func userThemeName(user *user_model.User) string {
if user == nil || user.Theme == "" {
return setting.UI.DefaultTheme
}
if webtheme.IsThemeAvailable(user.Theme) {
return user.Theme
}
return setting.UI.DefaultTheme
}
func isQueryParamEmpty(v any) bool {
return v == nil || v == false || v == 0 || v == int64(0) || v == ""
}

View File

@ -23,8 +23,10 @@ import (
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/webtheme"
)
type RenderUtils struct {
@ -259,3 +261,18 @@ func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink strin
htmlCode += "</span>"
return template.HTML(htmlCode)
}
func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML {
svgName := "octicon-paintbrush"
switch info.ColorScheme {
case "dark":
svgName = "octicon-moon"
case "light":
svgName = "octicon-sun"
case "auto":
svgName = "gitea-eclipse"
}
icon := svg.RenderHTML(svgName, iconSize)
extraIcon := svg.RenderHTML(info.GetExtraIconName(), iconSize)
return htmlutil.HTMLFormat(`<div class="theme-menu-item" data-tooltip-content="%s">%s %s %s</div>`, info.GetDescription(), icon, info.DisplayName, extraIcon)
}

View File

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// SetRedirectToCookie convenience function to set the RedirectTo cookie consistently
@ -39,11 +40,13 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
// These are more specific than cookies without a trailing /, so
// we need to delete these if they exist.
deleteLegacySiteCookie(resp, name)
// HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath
cookie := &http.Cookie{
Name: name,
Value: url.QueryEscape(value),
MaxAge: maxAge,
Path: setting.SessionConfig.CookiePath,
Path: util.IfZero(setting.SessionConfig.CookiePath, "/"),
Domain: setting.SessionConfig.Domain,
Secure: setting.SessionConfig.Secure,
HttpOnly: true,

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="svg gitea-colorblind-redgreen" width="16" height="16" aria-hidden="true"><g clip-path="url(#gitea-colorblind-redgreen__a)"><rect width="40" height="40" fill="#fff" rx="20"/><path fill="#0566d5" d="M34.284 34.284c7.81-7.81 7.81-20.474 0-28.284L6 34.284c7.81 7.81 20.474 7.81 28.284 0"/><path fill="#e7a100" d="M34.283 34.284c7.81-7.81 7.81-20.474 0-28.284L20.14 20.142z"/><circle cx="20" cy="20" r="18" fill="#0000" stroke="#aaa" stroke-width="4"/></g><defs><clipPath id="gitea-colorblind-redgreen__a"><rect width="40" height="40" fill="#fff" rx="20"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 654 B

1
public/assets/img/svg/gitea-eclipse.svg generated Normal file
View File

@ -0,0 +1 @@
<svg viewBox="490 490 820 820" class="svg gitea-eclipse" xmlns="http://www.w3.org/2000/svg" width="16" height="16" aria-hidden="true"><path d="M866.7 582.1A321.3 321.3 0 0 0 738.6 623a317.3 317.3 0 0 0-109.1 108.5A418 418 0 0 0 609 772a335.3 335.3 0 0 0-19.6 71.5 205.2 205.2 0 0 0-2.8 45c0 25.8.2 29.3 2.8 45 4.1 25.4 9.9 46.4 19.6 71.5a314.2 314.2 0 0 0 111.6 137.3A306.8 306.8 0 0 0 893 1196a308.6 308.6 0 0 0 303.6-262.5c2.6-15.7 2.8-19.2 2.8-45s-.2-29.3-2.8-45A335.3 335.3 0 0 0 1177 772a314.2 314.2 0 0 0-111.6-137.3A308.3 308.3 0 0 0 918 582c-13-1.1-38.2-1.1-51.3.1M747 663.5l-2.4 16.7c-4 26.4-4.9 41.1-4.3 65.3a323.7 323.7 0 0 0 37.2 145c18.2 36 41.3 66.6 72 95.5a346.4 346.4 0 0 0 208.5 93.1l18 1.6 4.5.5-8.5 8a259.3 259.3 0 0 1-141.5 65.8 281 281 0 0 1-123.9-11.4 267.2 267.2 0 0 1-181.7-269.9c2-27.6 5.7-47.6 13.3-70.7a281.2 281.2 0 0 1 46.4-85c8-10.1 28-30.2 37.9-38.1 13.8-11.1 24.5-18.3 24.5-16.4"/></svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@ -35,7 +35,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) {
httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true})
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
tmplCtx := context.TemplateContext{}
tmplCtx := context.NewTemplateContext(req.Context(), req)
tmplCtx["Locale"] = middleware.Locale(w, req)
ctxData := middleware.GetContextData(req.Context())

View File

@ -133,7 +133,7 @@ func renderServiceUnavailable(w http.ResponseWriter, req *http.Request) {
return
}
tmplCtx := giteacontext.TemplateContext{}
tmplCtx := giteacontext.NewTemplateContext(req.Context(), req)
tmplCtx["Locale"] = middleware.Locale(w, req)
ctxData := middleware.GetContextData(req.Context())
err := templates.HTMLRenderer().HTML(w, http.StatusServiceUnavailable, tplStatus503, ctxData, tmplCtx)

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/routers/web/misc"
"code.gitea.io/gitea/services/forms"
)
@ -32,7 +33,11 @@ func Routes() *web.Router {
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/post-install", InstallDone)
r.Get("/-/web-theme/list", misc.WebThemeList)
r.Post("/-/web-theme/apply", misc.WebThemeApply)
r.Get("/api/healthz", healthcheck.Check)
r.NotFound(installNotFound)
base.Mount("", r)

View File

@ -0,0 +1,41 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
"code.gitea.io/gitea/services/webtheme"
)
func WebThemeList(ctx *context.Context) {
curWebTheme := ctx.TemplateContext.CurrentWebTheme()
renderUtils := templates.NewRenderUtils(ctx)
allThemes := webtheme.GetAvailableThemes()
var results []map[string]any
for _, theme := range allThemes {
results = append(results, map[string]any{
"name": renderUtils.RenderThemeItem(theme, 14),
"value": theme.InternalName,
"class": "item js-aria-clickable" + util.Iif(theme.InternalName == curWebTheme.InternalName, " selected", ""),
})
}
ctx.JSON(http.StatusOK, map[string]any{"results": results})
}
func WebThemeApply(ctx *context.Context) {
themeName := ctx.FormString("theme")
middleware.SetSiteCookie(ctx.Resp, "gitea_theme", themeName, 0)
if ctx.Doer != nil {
opts := &user_service.UpdateOptions{Theme: optional.Some(themeName)}
_ = user_service.UpdateUser(ctx, ctx.Doer, opts)
}
}

View File

@ -369,7 +369,7 @@ func UpdateUIThemePost(ctx *context.Context) {
return
}
if !webtheme.IsThemeAvailable(form.Theme) {
if webtheme.GetThemeMetaInfo(form.Theme) == nil {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return

View File

@ -497,6 +497,9 @@ func registerWebRoutes(m *web.Router) {
m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)
m.Get("/-/web-theme/list", misc.WebThemeList)
m.Post("/-/web-theme/apply", optSignInIgnoreCsrf, misc.WebThemeApply)
m.Group("/explore", func() {
m.Get("", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/explore/repos")

View File

@ -103,7 +103,7 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
}
func NewTemplateContextForWeb(ctx *Context) TemplateContext {
tmplCtx := NewTemplateContext(ctx)
tmplCtx := NewTemplateContext(ctx, ctx.Req)
tmplCtx["Locale"] = ctx.Base.Locale
tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx)

View File

@ -5,13 +5,16 @@ package context
import (
"context"
"net/http"
"time"
"code.gitea.io/gitea/services/webtheme"
)
var _ context.Context = TemplateContext(nil)
func NewTemplateContext(ctx context.Context) TemplateContext {
return TemplateContext{"_ctx": ctx}
func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext {
return TemplateContext{"_ctx": ctx, "_req": req}
}
func (c TemplateContext) parentContext() context.Context {
@ -33,3 +36,19 @@ func (c TemplateContext) Err() error {
func (c TemplateContext) Value(key any) any {
return c.parentContext().Value(key)
}
func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo {
req := c["_req"].(*http.Request)
var themeName string
if webCtx := GetWebContext(c); webCtx != nil {
if webCtx.Doer != nil {
themeName = webCtx.Doer.Theme
}
}
if themeName == "" {
if cookieTheme, _ := req.Cookie("gitea_theme"); cookieTheme != nil {
themeName = cookieTheme.Value
}
}
return webtheme.GuaranteeGetThemeMetaInfo(themeName)
}

View File

@ -17,9 +17,9 @@ import (
)
var (
availableThemes []*ThemeMetaInfo
availableThemeInternalNames container.Set[string]
themeOnce sync.Once
availableThemes []*ThemeMetaInfo
availableThemeMap map[string]*ThemeMetaInfo
themeOnce sync.Once
)
const (
@ -28,9 +28,25 @@ const (
)
type ThemeMetaInfo struct {
FileName string
InternalName string
DisplayName string
FileName string
InternalName string
DisplayName string
ColorblindType string
ColorScheme string
}
func (info *ThemeMetaInfo) GetDescription() string {
if info.ColorblindType == "red-green" {
return "Red-green colorblind friendly"
}
return ""
}
func (info *ThemeMetaInfo) GetExtraIconName() string {
if info.ColorblindType == "red-green" {
return "gitea-colorblind-redgreen"
}
return ""
}
func parseThemeMetaInfoToMap(cssContent string) map[string]string {
@ -54,7 +70,7 @@ func parseThemeMetaInfoToMap(cssContent string) map[string]string {
|('(\\'|[^'])*')
|([^'";]+)
)
\s*;
\s*;?
\s*
)
`
@ -102,17 +118,19 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
return themeInfo
}
themeInfo.DisplayName = m["--theme-display-name"]
themeInfo.ColorblindType = m["--theme-colorblind-type"]
themeInfo.ColorScheme = m["--theme-color-scheme"]
return themeInfo
}
func initThemes() {
availableThemes = nil
defer func() {
availableThemeInternalNames = container.Set[string]{}
availableThemeMap = map[string]*ThemeMetaInfo{}
for _, theme := range availableThemes {
availableThemeInternalNames.Add(theme.InternalName)
availableThemeMap[theme.InternalName] = theme
}
if !availableThemeInternalNames.Contains(setting.UI.DefaultTheme) {
if availableThemeMap[setting.UI.DefaultTheme] == nil {
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
}
}()
@ -147,6 +165,9 @@ func initThemes() {
if availableThemes[i].InternalName == setting.UI.DefaultTheme {
return true
}
if availableThemes[i].ColorblindType != availableThemes[j].ColorblindType {
return availableThemes[i].ColorblindType < availableThemes[j].ColorblindType
}
return availableThemes[i].DisplayName < availableThemes[j].DisplayName
})
if len(availableThemes) == 0 {
@ -160,7 +181,18 @@ func GetAvailableThemes() []*ThemeMetaInfo {
return availableThemes
}
func IsThemeAvailable(internalName string) bool {
func GetThemeMetaInfo(internalName string) *ThemeMetaInfo {
themeOnce.Do(initThemes)
return availableThemeInternalNames.Contains(internalName)
return availableThemeMap[internalName]
}
func GuaranteeGetThemeMetaInfo(internalName string) *ThemeMetaInfo {
info := GetThemeMetaInfo(internalName)
if info == nil {
info = GetThemeMetaInfo(setting.UI.DefaultTheme)
}
if info == nil {
info = &ThemeMetaInfo{}
}
return info
}

View File

@ -34,4 +34,11 @@ gitea-theme-meta-info {
--k2: real;
}`)
assert.Equal(t, map[string]string{"--k2": "real"}, m)
// compressed CSS, no trailing semicolon
m = parseThemeMetaInfoToMap(`}gitea-theme-meta-info{--k1: "v1";--k2: "v2"}:`)
assert.Equal(t, map[string]string{
"--k1": "v1",
"--k2": "v2",
}, m)
}

View File

@ -17,6 +17,10 @@
{{end}}
</div>
<div class="right-links" role="group" aria-label="{{ctx.Locale.Tr "aria.footer.links"}}">
<div class="ui dropdown custom" id="footer-theme-selector">
<span class="default-text">{{ctx.RenderUtils.RenderThemeItem ctx.CurrentWebTheme 16}}</span>
<div class="menu theme-menu"></div>
</div>
<div class="ui dropdown upward">
<span class="flex-text-inline">{{svg "octicon-globe" 14}} {{ctx.Locale.LangName}}</span>
<div class="menu language-menu">

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ctx.Locale.Lang}}" data-theme="{{UserThemeName .SignedUser}}">
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{if .Title}}{{.Title}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>

View File

@ -1,2 +1,2 @@
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/index.css?v={{AssetVersion}}">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-{{UserThemeName .SignedUser | PathEscape}}.css?v={{AssetVersion}}">
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/theme-{{ctx.CurrentWebTheme.InternalName | PathEscape}}.css?v={{AssetVersion}}">

View File

@ -1,12 +1,12 @@
{{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics.
* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, UserThemeName
* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl
* ctx.Locale
* .Flash
* .ErrorMsg
* .SignedUser (optional)
*/}}
<!DOCTYPE html>
<html lang="{{ctx.Locale.Lang}}" data-theme="{{UserThemeName .SignedUser}}">
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Internal Server Error - {{AppName}}</title>

View File

@ -16,11 +16,19 @@
</div>
<div class="field">
<label>{{ctx.Locale.Tr "settings.ui"}}</label>
<select name="theme" class="ui dropdown">
{{range $theme := .AllThemes}}
<option value="{{$theme.InternalName}}" {{Iif (eq $.SignedUser.Theme $theme.InternalName) "selected"}}>{{$theme.DisplayName}}</option>
{{end}}
</select>
<div class="ui selection dropdown">
<input type="hidden" name="theme" value="{{$.SignedUser.Theme}}">
<div class="text"></div> {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu flex-items-menu">
{{range $theme := .AllThemes}}
{{$extraIconName := $theme.GetExtraIconName}}
<div class="item" data-value="{{$theme.InternalName}}">
{{$theme.DisplayName}} {{svg $extraIconName}}
<div class="description">{{$theme.GetDescription}}</div>
</div>
{{end}}
</div>
</div>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "settings.update_theme"}}</button>

View File

@ -65,15 +65,34 @@
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 1em;
}
.page-footer .right-links > a {
border-left: 1px solid var(--color-secondary-dark-1);
padding-left: 8px;
margin-left: 5px;
padding-left: 1em;
}
.page-footer .ui.dropdown .menu.language-menu {
/* the theme item is also used for the menu's "default text" display */
.page-footer .ui.dropdown .theme-menu-item {
display: flex;
align-items: center;
gap: 0.5em;
}
/* Fomantic UI dropdown "remote items by API" can't change parent "item" element,
so we use "theme-menu-item" in the "item" and add tooltip to the inner one.
Then the inner one needs to get padding and parent "item" padding needs to be removed */
.page-footer .menu.theme-menu > .item {
padding: 0 !important;
}
.page-footer .menu.theme-menu > .item > .theme-menu-item {
padding: 11px 16px;
}
.page-footer .ui.dropdown .menu.language-menu,
.page-footer .ui.dropdown .menu.theme-menu {
max-height: min(500px, calc(100vh - 60px));
overflow-y: auto;
margin-bottom: 10px;

View File

@ -2,5 +2,7 @@
@import "./theme-gitea-dark-protanopia-deuteranopia.css" (prefers-color-scheme: dark);
gitea-theme-meta-info {
--theme-display-name: "Auto (Red/Green Colorblind-friendly)";
--theme-display-name: "Auto";
--theme-colorblind-type: "red-green";
--theme-color-scheme: "auto";
}

View File

@ -3,4 +3,5 @@
gitea-theme-meta-info {
--theme-display-name: "Auto";
--theme-color-scheme: "auto";
}

View File

@ -1,7 +1,9 @@
@import "./theme-gitea-dark.css";
gitea-theme-meta-info {
--theme-display-name: "Dark (Red/Green Colorblind-friendly)";
--theme-display-name: "Dark";
--theme-colorblind-type: "red-green";
--theme-color-scheme: "dark";
}
/* red/green colorblind-friendly colors */

View File

@ -3,6 +3,7 @@
gitea-theme-meta-info {
--theme-display-name: "Dark";
--theme-color-scheme: "dark";
}
:root {

View File

@ -1,7 +1,9 @@
@import "./theme-gitea-light.css";
gitea-theme-meta-info {
--theme-display-name: "Light (Red/Green Colorblind-friendly)";
--theme-display-name: "Light";
--theme-colorblind-type: "red-green";
--theme-color-scheme: "light";
}
/* red/green colorblind-friendly colors */

View File

@ -3,6 +3,7 @@
gitea-theme-meta-info {
--theme-display-name: "Light";
--theme-color-scheme: "light";
}
:root {

View File

@ -1,14 +1,14 @@
import {GET} from '../modules/fetch.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showGlobalErrorMessage} from '../bootstrap.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {queryElems} from '../utils/dom.ts';
import {addDelegatedEventListener, queryElems} from '../utils/dom.ts';
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
import {initCompSearchRepoBox} from './comp/SearchRepoBox.ts';
const {appUrl} = window.config;
const {appUrl, appSubUrl} = window.config;
export function initHeadNavbarContentToggle() {
function initHeadNavbarContentToggle() {
const navbar = document.querySelector('#navbar');
const btn = document.querySelector('#navbar-expand-toggle');
if (!navbar || !btn) return;
@ -20,7 +20,7 @@ export function initHeadNavbarContentToggle() {
});
}
export function initFootLanguageMenu() {
function initFooterLanguageMenu() {
document.querySelector('.ui.dropdown .menu.language-menu')?.addEventListener('click', async (e) => {
const item = (e.target as HTMLElement).closest('.item');
if (!item) return;
@ -30,6 +30,26 @@ export function initFootLanguageMenu() {
});
}
function initFooterThemeSelector() {
const elDropdown = document.querySelector('#footer-theme-selector');
const $dropdown = fomanticQuery(elDropdown);
$dropdown.dropdown({
direction: 'upward',
apiSettings: {url: '/-/web-theme/list', cache: false},
});
addDelegatedEventListener(elDropdown, 'click', '.menu > .item', async (el) => {
const themeName = el.getAttribute('data-value');
await POST(`${appSubUrl}/-/web-theme/apply?theme=${encodeURIComponent(themeName)}`);
window.location.reload();
});
}
export function initCommmPageComponents() {
initHeadNavbarContentToggle();
initFooterLanguageMenu();
initFooterThemeSelector();
}
export function initGlobalDropdown() {
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
registerGlobalSelectorFunc('.ui.dropdown:not(.custom)', (el) => {

View File

@ -61,7 +61,7 @@ import {initColorPickers} from './features/colorpicker.ts';
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
import {initFootLanguageMenu, initGlobalComponent, initGlobalDropdown, initGlobalInput, initHeadNavbarContentToggle} from './features/common-page.ts';
import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts';
import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts';
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts';
@ -94,8 +94,7 @@ const initPerformanceTracer = callInitFunctions([
initInstall,
initHeadNavbarContentToggle,
initFootLanguageMenu,
initCommmPageComponents,
initContextPopups,
initHeatmap,

View File

@ -0,0 +1,13 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<rect width="40" height="40" rx="20" fill="white"/>
<path d="M34.2843 34.2842C42.0948 26.4737 42.0948 13.8104 34.2843 5.9999L6 34.2842C13.8105 42.0947 26.4738 42.0947 34.2843 34.2842Z" fill="#0566D5"/>
<path d="M34.2828 34.2842C42.0932 26.4737 42.0932 13.8104 34.2828 5.99995L20.1406 20.1421L34.2828 34.2842Z" fill="#E7A100"/>
<circle cx="20" cy="20" r="18" fill="#0000" stroke="#aaa" stroke-width="4"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="40" height="40" rx="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 678 B

View File

@ -0,0 +1 @@
<svg viewBox="490 490 820 820"><path d="M866.7 582.1A321.3 321.3 0 0 0 738.6 623a317.3 317.3 0 0 0-109.1 108.5A418 418 0 0 0 609 772a335.3 335.3 0 0 0-19.6 71.5 205.2 205.2 0 0 0-2.8 45c0 25.8.2 29.3 2.8 45 4.1 25.4 9.9 46.4 19.6 71.5a314.2 314.2 0 0 0 111.6 137.3A306.8 306.8 0 0 0 893 1196a308.6 308.6 0 0 0 303.6-262.5c2.6-15.7 2.8-19.2 2.8-45s-.2-29.3-2.8-45A335.3 335.3 0 0 0 1177 772a314.2 314.2 0 0 0-111.6-137.3A308.3 308.3 0 0 0 918 582c-13-1.1-38.2-1.1-51.3.1zM747 663.5l-2.4 16.7c-4 26.4-4.9 41.1-4.3 65.3a323.7 323.7 0 0 0 37.2 145c18.2 36 41.3 66.6 72 95.5a346.4 346.4 0 0 0 208.5 93.1l18 1.6 4.5.5-8.5 8a259.3 259.3 0 0 1-141.5 65.8 281 281 0 0 1-123.9-11.4 267.2 267.2 0 0 1-181.7-269.9c2-27.6 5.7-47.6 13.3-70.7a281.2 281.2 0 0 1 46.4-85c8-10.1 28-30.2 37.9-38.1 13.8-11.1 24.5-18.3 24.5-16.4z"/></svg>

After

Width:  |  Height:  |  Size: 818 B