Skip to content

Commit 7990e65

Browse files
committed
fix
1 parent 3e53b01 commit 7990e65

19 files changed

+13995
-49
lines changed

custom/conf/app.example.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,9 @@ LEVEL = Info
12941294
;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css"
12951295
;THEMES =
12961296
;;
1297+
;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future.
1298+
;FILE_ICON_THEME = material
1299+
;;
12971300
;; All available reactions users can choose on issues/prs and comments.
12981301
;; Values can be emoji alias (:smile:) or a unicode emoji.
12991302
;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png

modules/fileicon/basic.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package fileicon
5+
6+
import (
7+
"context"
8+
"html/template"
9+
10+
"code.gitea.io/gitea/modules/git"
11+
"code.gitea.io/gitea/modules/svg"
12+
)
13+
14+
func FileIconBasic(ctx context.Context, entry *git.TreeEntry) template.HTML {
15+
svgName := "octicon-file"
16+
switch {
17+
case entry.IsLink():
18+
svgName = "octicon-file-symlink-file"
19+
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
20+
svgName = "octicon-file-directory-symlink"
21+
}
22+
case entry.IsDir():
23+
svgName = "octicon-file-directory-fill"
24+
case entry.IsSubModule():
25+
svgName = "octicon-file-submodule"
26+
}
27+
return svg.RenderHTML(svgName)
28+
}

modules/fileicon/material.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package fileicon
5+
6+
import (
7+
"html/template"
8+
"net/http"
9+
"path"
10+
"strings"
11+
"sync"
12+
13+
"code.gitea.io/gitea/modules/git"
14+
"code.gitea.io/gitea/modules/json"
15+
"code.gitea.io/gitea/modules/log"
16+
"code.gitea.io/gitea/modules/options"
17+
"code.gitea.io/gitea/modules/reqctx"
18+
"code.gitea.io/gitea/modules/svg"
19+
)
20+
21+
type materialIconRulesData struct {
22+
IconDefinitions map[string]*struct {
23+
IconPath string `json:"iconPath"`
24+
} `json:"iconDefinitions"`
25+
FileNames map[string]string `json:"fileNames"`
26+
FolderNames map[string]string `json:"folderNames"`
27+
FileExtensions map[string]string `json:"fileExtensions"`
28+
LanguageIds map[string]string `json:"languageIds"`
29+
}
30+
31+
type MaterialIconProvider struct {
32+
once sync.Once
33+
fs http.FileSystem
34+
rules *materialIconRulesData
35+
svgs map[string]string
36+
}
37+
38+
var materialIconProvider MaterialIconProvider
39+
40+
func DefaultMaterialIconProvider() *MaterialIconProvider {
41+
return &materialIconProvider
42+
}
43+
44+
func (m *MaterialIconProvider) loadData() {
45+
buf, err := options.AssetFS().ReadFile("fileicon/material-icon-rules.json")
46+
if err != nil {
47+
log.Error("Failed to read material icon rules: %v", err)
48+
return
49+
}
50+
err = json.Unmarshal(buf, &m.rules)
51+
if err != nil {
52+
log.Error("Failed to unmarshal material icon rules: %v", err)
53+
return
54+
}
55+
56+
buf, err = options.AssetFS().ReadFile("fileicon/material-icon-svgs.json")
57+
if err != nil {
58+
log.Error("Failed to read material icon rules: %v", err)
59+
return
60+
}
61+
err = json.Unmarshal(buf, &m.svgs)
62+
if err != nil {
63+
log.Error("Failed to unmarshal material icon rules: %v", err)
64+
return
65+
}
66+
log.Debug("Loaded material icon rules and SVG images")
67+
}
68+
69+
func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg string) template.HTML {
70+
data := ctx.GetData()
71+
renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool)
72+
if renderedSVGs == nil {
73+
renderedSVGs = make(map[string]bool)
74+
data["_RenderedSVGs"] = renderedSVGs
75+
}
76+
// This part is a bit hacky, but it works really well. It should be safe to do so because all SVG icons are generated by us.
77+
// Will try to refactor this in the future.
78+
if !strings.HasPrefix(svg, "<svg") {
79+
panic("Invalid SVG icon")
80+
}
81+
svgID := "svg-mfi-" + name
82+
svgCommonAttrs := `class="svg fileicon" width="16" height="16" aria-hidden="true"`
83+
posOuterBefore := strings.IndexByte(svg, '>')
84+
if renderedSVGs[svgID] && posOuterBefore != -1 {
85+
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
86+
}
87+
svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]
88+
renderedSVGs[svgID] = true
89+
return template.HTML(svg)
90+
}
91+
92+
func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
93+
m.once.Do(m.loadData)
94+
95+
if m.rules == nil {
96+
return FileIconBasic(ctx, entry)
97+
}
98+
99+
if entry.IsLink() {
100+
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
101+
return svg.RenderHTML("material-folder-symlink")
102+
}
103+
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
104+
}
105+
106+
name := m.findIconName(entry)
107+
if name == "folder" {
108+
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
109+
return svg.RenderHTML("material-folder-generic")
110+
}
111+
if iconSVG, ok := m.svgs[name]; ok && iconSVG != "" {
112+
return m.renderFileIconSVG(ctx, name, iconSVG)
113+
}
114+
return svg.RenderHTML("octicon-file")
115+
}
116+
117+
func (m *MaterialIconProvider) findIconName(entry *git.TreeEntry) string {
118+
if entry.IsSubModule() {
119+
return "folder-git"
120+
}
121+
122+
iconsData := m.rules
123+
fileName := path.Base(entry.Name())
124+
125+
if entry.IsDir() {
126+
if s, ok := iconsData.FolderNames[fileName]; ok {
127+
return s
128+
}
129+
if s, ok := iconsData.FolderNames[strings.ToLower(fileName)]; ok {
130+
return s
131+
}
132+
return "folder"
133+
}
134+
135+
if s, ok := iconsData.FileNames[fileName]; ok {
136+
return s
137+
}
138+
if s, ok := iconsData.FileNames[strings.ToLower(fileName)]; ok {
139+
return s
140+
}
141+
142+
for i := len(fileName) - 1; i >= 0; i-- {
143+
if fileName[i] == '.' {
144+
ext := fileName[i+1:]
145+
if s, ok := iconsData.FileExtensions[ext]; ok {
146+
return s
147+
}
148+
}
149+
}
150+
151+
return "file"
152+
}

modules/reqctx/datastore.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ type RequestContext interface {
9494
}
9595

9696
func FromContext(ctx context.Context) RequestContext {
97+
if rc, ok := ctx.(RequestContext); ok {
98+
return rc
99+
}
97100
// here we must use the current ctx and the underlying store
98101
// the current ctx guarantees that the ctx deadline/cancellation/values are respected
99102
// the underlying store guarantees that the request-specific data is available
@@ -134,6 +137,6 @@ func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Co
134137

135138
// NewRequestContextForTest creates a new RequestContext for testing purposes
136139
// It doesn't add the context to the process manager, nor do cleanup
137-
func NewRequestContextForTest(parentCtx context.Context) context.Context {
140+
func NewRequestContextForTest(parentCtx context.Context) RequestContext {
138141
return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}}
139142
}

modules/setting/ui.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var UI = struct {
2828
DefaultShowFullName bool
2929
DefaultTheme string
3030
Themes []string
31+
FileIconTheme string
3132
Reactions []string
3233
ReactionsLookup container.Set[string] `ini:"-"`
3334
CustomEmojis []string
@@ -84,6 +85,7 @@ var UI = struct {
8485
ReactionMaxUserNum: 10,
8586
MaxDisplayFileSize: 8388608,
8687
DefaultTheme: `gitea-auto`,
88+
FileIconTheme: `material`,
8789
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
8890
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
8991
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},

modules/templates/util_render.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package templates
55

66
import (
7-
"context"
87
"encoding/hex"
98
"fmt"
109
"html/template"
@@ -16,20 +15,23 @@ import (
1615

1716
issues_model "code.gitea.io/gitea/models/issues"
1817
"code.gitea.io/gitea/modules/emoji"
18+
"code.gitea.io/gitea/modules/fileicon"
19+
"code.gitea.io/gitea/modules/git"
1920
"code.gitea.io/gitea/modules/htmlutil"
2021
"code.gitea.io/gitea/modules/log"
2122
"code.gitea.io/gitea/modules/markup"
2223
"code.gitea.io/gitea/modules/markup/markdown"
24+
"code.gitea.io/gitea/modules/reqctx"
2325
"code.gitea.io/gitea/modules/setting"
2426
"code.gitea.io/gitea/modules/translation"
2527
"code.gitea.io/gitea/modules/util"
2628
)
2729

2830
type RenderUtils struct {
29-
ctx context.Context
31+
ctx reqctx.RequestContext
3032
}
3133

32-
func NewRenderUtils(ctx context.Context) *RenderUtils {
34+
func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils {
3335
return &RenderUtils{ctx: ctx}
3436
}
3537

@@ -179,6 +181,13 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
179181
textColor, itemColor, itemHTML)
180182
}
181183

184+
func (ut *RenderUtils) RenderFileIcon(entry *git.TreeEntry) template.HTML {
185+
if setting.UI.FileIconTheme == "material" {
186+
return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, entry)
187+
}
188+
return fileicon.FileIconBasic(ut.ctx, entry)
189+
}
190+
182191
// RenderEmoji renders html text with emoji post processors
183192
func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
184193
renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))

modules/templates/util_render_legacy.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,46 @@ import (
88
"html/template"
99

1010
issues_model "code.gitea.io/gitea/models/issues"
11+
"code.gitea.io/gitea/modules/reqctx"
1112
"code.gitea.io/gitea/modules/translation"
1213
)
1314

1415
func renderEmojiLegacy(ctx context.Context, text string) template.HTML {
1516
panicIfDevOrTesting()
16-
return NewRenderUtils(ctx).RenderEmoji(text)
17+
return NewRenderUtils(reqctx.FromContext(ctx)).RenderEmoji(text)
1718
}
1819

1920
func renderLabelLegacy(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
2021
panicIfDevOrTesting()
21-
return NewRenderUtils(ctx).RenderLabel(label)
22+
return NewRenderUtils(reqctx.FromContext(ctx)).RenderLabel(label)
2223
}
2324

2425
func renderLabelsLegacy(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML {
2526
panicIfDevOrTesting()
26-
return NewRenderUtils(ctx).RenderLabels(labels, repoLink, issue)
27+
return NewRenderUtils(reqctx.FromContext(ctx)).RenderLabels(labels, repoLink, issue)
2728
}
2829

2930
func renderMarkdownToHtmlLegacy(ctx context.Context, input string) template.HTML { //nolint:revive
3031
panicIfDevOrTesting()
31-
return NewRenderUtils(ctx).MarkdownToHtml(input)
32+
return NewRenderUtils(reqctx.FromContext(ctx)).MarkdownToHtml(input)
3233
}
3334

3435
func renderCommitMessageLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML {
3536
panicIfDevOrTesting()
36-
return NewRenderUtils(ctx).RenderCommitMessage(msg, metas)
37+
return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessage(msg, metas)
3738
}
3839

3940
func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML {
4041
panicIfDevOrTesting()
41-
return NewRenderUtils(ctx).RenderCommitMessageLinkSubject(msg, urlDefault, metas)
42+
return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessageLinkSubject(msg, urlDefault, metas)
4243
}
4344

4445
func renderIssueTitleLegacy(ctx context.Context, text string, metas map[string]string) template.HTML {
4546
panicIfDevOrTesting()
46-
return NewRenderUtils(ctx).RenderIssueTitle(text, metas)
47+
return NewRenderUtils(reqctx.FromContext(ctx)).RenderIssueTitle(text, metas)
4748
}
4849

4950
func renderCommitBodyLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML {
5051
panicIfDevOrTesting()
51-
return NewRenderUtils(ctx).RenderCommitBody(msg, metas)
52+
return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitBody(msg, metas)
5253
}

0 commit comments

Comments
 (0)