mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +00:00
Keep file tree view icons consistent with icon theme (#33921)
Fix #33914 before:  after:  --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
)
|
||||
|
||||
@@ -62,13 +61,7 @@ func (m *MaterialIconProvider) loadData() {
|
||||
log.Debug("Loaded material icon rules and SVG images")
|
||||
}
|
||||
|
||||
func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg, extraClass string) template.HTML {
|
||||
data := ctx.GetData()
|
||||
renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool)
|
||||
if renderedSVGs == nil {
|
||||
renderedSVGs = make(map[string]bool)
|
||||
data["_RenderedSVGs"] = renderedSVGs
|
||||
}
|
||||
func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg, extraClass string) template.HTML {
|
||||
// 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.
|
||||
// Will try to refactor this in the future.
|
||||
if !strings.HasPrefix(svg, "<svg") {
|
||||
@@ -76,16 +69,13 @@ func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name
|
||||
}
|
||||
svgID := "svg-mfi-" + name
|
||||
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
|
||||
posOuterBefore := strings.IndexByte(svg, '>')
|
||||
if renderedSVGs[svgID] && posOuterBefore != -1 {
|
||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
||||
if p.IconSVGs[svgID] == "" {
|
||||
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
||||
}
|
||||
svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]
|
||||
renderedSVGs[svgID] = true
|
||||
return template.HTML(svg)
|
||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
||||
}
|
||||
|
||||
func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
|
||||
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||
if m.rules == nil {
|
||||
return BasicThemeIcon(entry)
|
||||
}
|
||||
@@ -110,7 +100,7 @@ func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.Tr
|
||||
case entry.IsSubModule():
|
||||
extraClass = "octicon-file-submodule"
|
||||
}
|
||||
return m.renderFileIconSVG(ctx, name, iconSVG, extraClass)
|
||||
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
|
||||
}
|
||||
// TODO: use an interface or wrapper for git.Entry to make the code testable.
|
||||
return BasicThemeIcon(entry)
|
||||
|
52
modules/fileicon/render.go
Normal file
52
modules/fileicon/render.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fileicon
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type RenderedIconPool struct {
|
||||
IconSVGs map[string]template.HTML
|
||||
}
|
||||
|
||||
func NewRenderedIconPool() *RenderedIconPool {
|
||||
return &RenderedIconPool{
|
||||
IconSVGs: make(map[string]template.HTML),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *RenderedIconPool) RenderToHTML() template.HTML {
|
||||
if len(p.IconSVGs) == 0 {
|
||||
return ""
|
||||
}
|
||||
sb := &strings.Builder{}
|
||||
sb.WriteString(`<div class=tw-hidden>`)
|
||||
for _, icon := range p.IconSVGs {
|
||||
sb.WriteString(string(icon))
|
||||
}
|
||||
sb.WriteString(`</div>`)
|
||||
return template.HTML(sb.String())
|
||||
}
|
||||
|
||||
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
|
||||
|
||||
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||
if setting.UI.FileIconTheme == "material" {
|
||||
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
||||
}
|
||||
return BasicThemeIcon(entry)
|
||||
}
|
||||
|
||||
func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||
// TODO: add "open icon" support
|
||||
if setting.UI.FileIconTheme == "material" {
|
||||
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
||||
}
|
||||
return BasicThemeIcon(entry)
|
||||
}
|
@@ -32,19 +32,19 @@ func (err ErrNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrBadLink entry.FollowLink error
|
||||
type ErrBadLink struct {
|
||||
// ErrSymlinkUnresolved entry.FollowLink error
|
||||
type ErrSymlinkUnresolved struct {
|
||||
Name string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err ErrBadLink) Error() string {
|
||||
func (err ErrSymlinkUnresolved) Error() string {
|
||||
return fmt.Sprintf("%s: %s", err.Name, err.Message)
|
||||
}
|
||||
|
||||
// IsErrBadLink if some error is ErrBadLink
|
||||
func IsErrBadLink(err error) bool {
|
||||
_, ok := err.(ErrBadLink)
|
||||
// IsErrSymlinkUnresolved if some error is ErrSymlinkUnresolved
|
||||
func IsErrSymlinkUnresolved(err error) bool {
|
||||
_, ok := err.(ErrSymlinkUnresolved)
|
||||
return ok
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,8 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// Type returns the type of the entry (commit, tree, blob)
|
||||
@@ -25,7 +27,7 @@ func (te *TreeEntry) Type() string {
|
||||
// FollowLink returns the entry pointed to by a symlink
|
||||
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||
if !te.IsLink() {
|
||||
return nil, ErrBadLink{te.Name(), "not a symlink"}
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
|
||||
}
|
||||
|
||||
// read the link
|
||||
@@ -56,13 +58,13 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
return nil, ErrBadLink{te.Name(), "points outside of repo"}
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "points outside of repo"}
|
||||
}
|
||||
|
||||
target, err := t.GetTreeEntryByPath(lnk)
|
||||
if err != nil {
|
||||
if IsErrNotExist(err) {
|
||||
return nil, ErrBadLink{te.Name(), "broken link"}
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "broken link"}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,33 +72,27 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||
}
|
||||
|
||||
// FollowLinks returns the entry ultimately pointed to by a symlink
|
||||
func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
|
||||
func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) {
|
||||
if !te.IsLink() {
|
||||
return nil, ErrBadLink{te.Name(), "not a symlink"}
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
|
||||
}
|
||||
limit := util.OptionalArg(optLimit, 10)
|
||||
entry := te
|
||||
for i := 0; i < 999; i++ {
|
||||
if entry.IsLink() {
|
||||
next, err := entry.FollowLink()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next.ID == entry.ID {
|
||||
return nil, ErrBadLink{
|
||||
entry.Name(),
|
||||
"recursive link",
|
||||
}
|
||||
}
|
||||
entry = next
|
||||
} else {
|
||||
for i := 0; i < limit; i++ {
|
||||
if !entry.IsLink() {
|
||||
break
|
||||
}
|
||||
next, err := entry.FollowLink()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next.ID == entry.ID {
|
||||
return nil, ErrSymlinkUnresolved{entry.Name(), "recursive link"}
|
||||
}
|
||||
entry = next
|
||||
}
|
||||
if entry.IsLink() {
|
||||
return nil, ErrBadLink{
|
||||
te.Name(),
|
||||
"too many levels of symbolic links",
|
||||
}
|
||||
return nil, ErrSymlinkUnresolved{te.Name(), "too many levels of symbolic links"}
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
@@ -17,16 +17,12 @@ const (
|
||||
// EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of
|
||||
// added the base commit will not have the file in its tree so a mode of 0o000000 is used.
|
||||
EntryModeNoEntry EntryMode = 0o000000
|
||||
// EntryModeBlob
|
||||
EntryModeBlob EntryMode = 0o100644
|
||||
// EntryModeExec
|
||||
EntryModeExec EntryMode = 0o100755
|
||||
// EntryModeSymlink
|
||||
|
||||
EntryModeBlob EntryMode = 0o100644
|
||||
EntryModeExec EntryMode = 0o100755
|
||||
EntryModeSymlink EntryMode = 0o120000
|
||||
// EntryModeCommit
|
||||
EntryModeCommit EntryMode = 0o160000
|
||||
// EntryModeTree
|
||||
EntryModeTree EntryMode = 0o040000
|
||||
EntryModeCommit EntryMode = 0o160000
|
||||
EntryModeTree EntryMode = 0o040000
|
||||
)
|
||||
|
||||
// String converts an EntryMode to a string
|
||||
@@ -34,12 +30,6 @@ func (e EntryMode) String() string {
|
||||
return strconv.FormatInt(int64(e), 8)
|
||||
}
|
||||
|
||||
// ToEntryMode converts a string to an EntryMode
|
||||
func ToEntryMode(value string) EntryMode {
|
||||
v, _ := strconv.ParseInt(value, 8, 32)
|
||||
return EntryMode(v)
|
||||
}
|
||||
|
||||
func ParseEntryMode(mode string) (EntryMode, error) {
|
||||
switch mode {
|
||||
case "000000":
|
||||
|
@@ -15,8 +15,6 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@@ -181,13 +179,6 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
|
||||
textColor, itemColor, itemHTML)
|
||||
}
|
||||
|
||||
func (ut *RenderUtils) RenderFileIcon(entry *git.TreeEntry) template.HTML {
|
||||
if setting.UI.FileIconTheme == "material" {
|
||||
return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, entry)
|
||||
}
|
||||
return fileicon.BasicThemeIcon(entry)
|
||||
}
|
||||
|
||||
// RenderEmoji renders html text with emoji post processors
|
||||
func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
|
||||
renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))
|
||||
|
Reference in New Issue
Block a user