Skip to content

Commit 470aa92

Browse files
committed
fix
1 parent 3f1f808 commit 470aa92

18 files changed

+13977
-41
lines changed

Diff for: custom/conf/app.example.ini

+3
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

Diff for: modules/fileicon/basic.go

+28
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+
}

Diff for: modules/fileicon/material.go

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
svgID := "svg-mfi-" + name
79+
posOuterBefore := strings.IndexByte(svg, '>')
80+
if renderedSVGs[svgID] {
81+
if posOuterBefore != -1 {
82+
svgOuterBefore := svg[:posOuterBefore+1]
83+
svgOuterBefore = strings.ReplaceAll(svgOuterBefore, ` viewBox=`, ` viewBoxOld=`)
84+
return template.HTML(svgOuterBefore + `<use xlink:href="#` + svgID + `"></use></svg>`)
85+
}
86+
}
87+
if strings.HasPrefix(svg, "<svg ") && strings.HasSuffix(svg, "</svg>") {
88+
svg = `<svg id="` + svgID + `" ` + svg[5:]
89+
renderedSVGs[svgID] = true
90+
}
91+
return template.HTML(svg)
92+
}
93+
94+
func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
95+
m.once.Do(m.loadData)
96+
97+
if m.rules == nil {
98+
return FileIconBasic(ctx, entry)
99+
}
100+
101+
if entry.IsLink() {
102+
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
103+
return svg.RenderHTML("material-folder-symlink")
104+
}
105+
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
106+
}
107+
108+
name := m.findIconName(entry)
109+
if name == "folder" {
110+
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
111+
return svg.RenderHTML("material-folder-generic")
112+
}
113+
if iconSVG, ok := m.svgs[name]; ok && iconSVG != "" {
114+
return m.renderFileIconSVG(ctx, name, iconSVG)
115+
}
116+
return svg.RenderHTML("octicon-file")
117+
}
118+
119+
func (m *MaterialIconProvider) findIconName(entry *git.TreeEntry) string {
120+
if entry.IsSubModule() {
121+
return "folder-git"
122+
}
123+
124+
iconsData := m.rules
125+
fileName := path.Base(entry.Name())
126+
127+
if entry.IsDir() {
128+
if s, ok := iconsData.FolderNames[fileName]; ok {
129+
return s
130+
}
131+
if s, ok := iconsData.FolderNames[strings.ToLower(fileName)]; ok {
132+
return s
133+
}
134+
return "folder"
135+
}
136+
137+
if s, ok := iconsData.FileNames[fileName]; ok {
138+
return s
139+
}
140+
if s, ok := iconsData.FileNames[strings.ToLower(fileName)]; ok {
141+
return s
142+
}
143+
144+
for i := len(fileName) - 1; i >= 0; i-- {
145+
if fileName[i] == '.' {
146+
ext := fileName[i+1:]
147+
if s, ok := iconsData.FileExtensions[ext]; ok {
148+
return s
149+
}
150+
}
151+
}
152+
153+
return "file"
154+
}

Diff for: modules/reqctx/datastore.go

+3
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

Diff for: modules/setting/ui.go

+2
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:"},

Diff for: modules/templates/util_render.go

+12-3
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))

Diff for: modules/templates/util_render_legacy.go

+9-8
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)