mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 05:55:21 +08:00
Compare commits
5 Commits
cb06b30ca8
...
4f9237c0ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f9237c0ec | ||
|
|
bc50431e8b | ||
|
|
2a6af15448 | ||
|
|
cb69979425 | ||
|
|
e884b35c87 |
2
go.mod
2
go.mod
@ -109,7 +109,7 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
|
||||
github.com/urfave/cli/v3 v3.4.1
|
||||
github.com/wneessen/go-mail v0.7.1
|
||||
github.com/wneessen/go-mail v0.7.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/yohcop/openid-go v1.0.1
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
|
||||
4
go.sum
4
go.sum
@ -768,8 +768,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/wneessen/go-mail v0.7.1 h1:rvy63sp14N06/kdGqCYwW8Na5gDCXjTQM1E7So4PuKk=
|
||||
github.com/wneessen/go-mail v0.7.1/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 == ""
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -3586,6 +3586,7 @@ variables.update.success=Proměnná byla upravena.
|
||||
logs.always_auto_scroll=Vždy automaticky posouvat logy
|
||||
logs.always_expand_running=Vždy rozšířit běžící logy
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Odstraněný projekt
|
||||
type-1.display_name=Samostatný projekt
|
||||
|
||||
@ -3645,6 +3645,7 @@ variables.update.success=Die Variable wurde bearbeitet.
|
||||
logs.always_auto_scroll=Autoscroll für Logs immer aktivieren
|
||||
logs.always_expand_running=Laufende Logs immer erweitern
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Gelöschtes Projekt
|
||||
type-1.display_name=Individuelles Projekt
|
||||
|
||||
@ -3280,6 +3280,7 @@ variables.update.failed=Αποτυχία επεξεργασίας μεταβλη
|
||||
variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Ατομικό Έργο
|
||||
type-2.display_name=Έργο Αποθετηρίου
|
||||
|
||||
@ -3257,6 +3257,7 @@ variables.update.failed=Error al editar la variable.
|
||||
variables.update.success=La variable ha sido editada.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Proyecto individual
|
||||
type-2.display_name=Proyecto repositorio
|
||||
|
||||
@ -2446,6 +2446,7 @@ runs.commit=کامیت
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1693,6 +1693,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3906,6 +3906,7 @@ variables.update.success=La variable a bien été modifiée.
|
||||
logs.always_auto_scroll=Toujours faire défiler les journaux automatiquement
|
||||
logs.always_expand_running=Toujours développer les journaux en cours
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Projet supprimé
|
||||
type-1.display_name=Projet personnel
|
||||
|
||||
@ -3914,6 +3914,7 @@ variables.update.success=Tá an t-athróg curtha in eagar.
|
||||
logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí
|
||||
logs.always_expand_running=Leathnaigh logs reatha i gcónaí
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Tionscadal scriosta
|
||||
type-1.display_name=Tionscadal Aonair
|
||||
|
||||
@ -1605,6 +1605,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1428,6 +1428,7 @@ variables.update.failed=Gagal mengedit variabel.
|
||||
variables.update.success=Variabel telah diedit.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Proyek Individu
|
||||
type-2.display_name=Proyek Repositori
|
||||
|
||||
@ -1334,6 +1334,7 @@ runs.commit=Framlag
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -2706,6 +2706,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3910,6 +3910,7 @@ variables.update.success=変数を更新しました。
|
||||
logs.always_auto_scroll=常にログを自動スクロール
|
||||
logs.always_expand_running=常に実行中のログを展開
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=削除されたプロジェクト
|
||||
type-1.display_name=個人プロジェクト
|
||||
|
||||
@ -1554,6 +1554,7 @@ runs.commit=커밋
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3282,6 +3282,7 @@ variables.update.failed=Neizdevās labot mainīgo.
|
||||
variables.update.success=Mainīgais tika labots.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Individuālais projekts
|
||||
type-2.display_name=Repozitorija projekts
|
||||
|
||||
@ -2458,6 +2458,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -2347,6 +2347,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3615,6 +3615,7 @@ variables.update.failed=Falha ao editar a variável.
|
||||
variables.update.success=A variável foi editada.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Excluir Projeto
|
||||
type-1.display_name=Projeto Individual
|
||||
|
||||
@ -3914,6 +3914,7 @@ variables.update.success=A variável foi editada.
|
||||
logs.always_auto_scroll=Rolar registos de forma automática e permanente
|
||||
logs.always_expand_running=Expandir sempre os registos que vão rolando
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Planeamento eliminado
|
||||
type-1.display_name=Planeamento individual
|
||||
|
||||
@ -3225,6 +3225,7 @@ variables.update.failed=Не удалось изменить переменну
|
||||
variables.update.success=Переменная изменена.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Индивидуальный проект
|
||||
type-2.display_name=Проект репозитория
|
||||
|
||||
@ -2391,6 +2391,7 @@ runs.commit=කැප
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1292,6 +1292,7 @@ runners.labels=Štítky
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1968,6 +1968,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3907,6 +3907,7 @@ variables.update.success=Değişken düzenlendi.
|
||||
logs.always_auto_scroll=Günlükleri her zaman otomatik kaydır
|
||||
logs.always_expand_running=Çalıştırma günlüklerini her zaman genişlet
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Silinmiş Proje
|
||||
type-1.display_name=Kişisel Proje
|
||||
|
||||
@ -3428,6 +3428,7 @@ variables.update.success=Змінну відредаговано.
|
||||
logs.always_auto_scroll=Завжди автоматично прокручувати журнали
|
||||
logs.always_expand_running=Завжди розгортати поточні журнали
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Видалений проєкт
|
||||
type-1.display_name=Індивідуальний проєкт
|
||||
|
||||
@ -3911,6 +3911,7 @@ variables.update.success=变量已编辑。
|
||||
logs.always_auto_scroll=总是自动滚动日志
|
||||
logs.always_expand_running=总是展开运行日志
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=已删除项目
|
||||
type-1.display_name=个人项目
|
||||
|
||||
@ -980,6 +980,7 @@ runners.task_list.repository=儲存庫
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3554,6 +3554,7 @@ variables.update.failed=編輯變數失敗。
|
||||
variables.update.success=已編輯變數。
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=已刪除的專案
|
||||
type-1.display_name=個人專案
|
||||
|
||||
1
public/assets/img/svg/gitea-colorblind-redgreen.svg
generated
Normal file
1
public/assets/img/svg/gitea-colorblind-redgreen.svg
generated
Normal 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
1
public/assets/img/svg/gitea-eclipse.svg
generated
Normal 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 |
@ -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())
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
41
routers/web/misc/webtheme.go
Normal file
41
routers/web/misc/webtheme.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -3,4 +3,5 @@
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Auto";
|
||||
--theme-color-scheme: "auto";
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Dark";
|
||||
--theme-color-scheme: "dark";
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
gitea-theme-meta-info {
|
||||
--theme-display-name: "Light";
|
||||
--theme-color-scheme: "light";
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
13
web_src/svg/gitea-colorblind-redgreen.svg
Normal file
13
web_src/svg/gitea-colorblind-redgreen.svg
Normal 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 |
1
web_src/svg/gitea-eclipse.svg
Normal file
1
web_src/svg/gitea-eclipse.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user