From c502126eb229e976e86564ce13187bc1b6f0c7da Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 30 Jul 2023 22:26:39 +0800 Subject: [PATCH 1/3] file icon --- .gitattributes | 1 + Makefile | 15 +- build/generate-options-fileicon.go | 69 ++++++ ...nores.go => generate-options-gitignore.go} | 0 ...icenses.go => generate-options-license.go} | 0 modules/base/tool.go | 24 --- modules/fileicon/basic.go | 33 +++ modules/fileicon/material.go | 201 ++++++++++++++++++ modules/templates/helper.go | 3 +- options/fileicon/material.tgz | 3 + templates/repo/view_list.tmpl | 4 +- 11 files changed, 316 insertions(+), 37 deletions(-) create mode 100644 build/generate-options-fileicon.go rename build/{generate-gitignores.go => generate-options-gitignore.go} (100%) rename build/{generate-licenses.go => generate-options-license.go} (100%) create mode 100644 modules/fileicon/basic.go create mode 100644 modules/fileicon/material.go create mode 100644 options/fileicon/material.tgz diff --git a/.gitattributes b/.gitattributes index 467b8a47b5d6d..e581c597d2c7e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,4 @@ /web_src/fomantic/_site/globals/site.variables linguist-language=Less /web_src/js/vendor/** -text -eol linguist-vendored Dockerfile.* linguist-language=Dockerfile +options/fileicon/material.tgz filter=lfs diff=lfs merge=lfs -text diff --git a/Makefile b/Makefile index 167f56c6b9266..90413ac81efaa 100644 --- a/Makefile +++ b/Makefile @@ -232,9 +232,8 @@ help: @echo " - fomantic build fomantic files" @echo " - generate run \"go generate\"" @echo " - fmt format the Go code" - @echo " - generate-license update license files" - @echo " - generate-gitignore update gitignore files" @echo " - generate-manpage generate manpage" + @echo " - generate-options generate licenses/gitignores/fileicons in options directory" @echo " - generate-swagger generate the swagger spec from code comments" @echo " - swagger-validate check if the swagger spec is valid" @echo " - go-licenses regenerate go licenses" @@ -990,13 +989,11 @@ update-translations: mv ./translations/*.ini ./options/locale/ rmdir ./translations -.PHONY: generate-license -generate-license: - $(GO) run build/generate-licenses.go - -.PHONY: generate-gitignore -generate-gitignore: - $(GO) run build/generate-gitignores.go +.PHONY: generate-options +generate-options: + $(GO) run build/generate-options-license.go + $(GO) run build/generate-options-gitignore.go + $(GO) run build/generate-options-fileicon.go .PHONY: generate-images generate-images: | node_modules diff --git a/build/generate-options-fileicon.go b/build/generate-options-fileicon.go new file mode 100644 index 0000000000000..76538c259103e --- /dev/null +++ b/build/generate-options-fileicon.go @@ -0,0 +1,69 @@ +//go:build ignore + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" +) + +func main() { + var destination string + flag.StringVar(&destination, "dest", "options/fileicon/", "destination for the fileicon") + flag.Parse() + + pkgName := "material-icon-theme" + req, err := http.NewRequest("GET", fmt.Sprintf("https://registry.npmjs.org/%s/", pkgName), nil) + if err != nil { + log.Fatalf("http req: %s", err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("http error: %s", err) + } + d := json.NewDecoder(resp.Body) + defer resp.Body.Close() + var m struct { + DistTags map[string]string `json:"dist-tags"` + } + err = d.Decode(&m) + if err != nil { + log.Fatalf("json decode: %s", err) + } + + latestTag := m.DistTags["latest"] + if latestTag == "" { + log.Fatal("no latest tag") + } + + pkg := fmt.Sprintf("https://registry.npmjs.org/%s/-/%s-%s.tgz", pkgName, pkgName, latestTag) + req, err = http.NewRequest("GET", pkg, nil) + if err != nil { + log.Fatalf("http req: %s", err) + } + resp, err = http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("http error: %s", err) + } + defer resp.Body.Close() + + localFileName := filepath.Join(destination, "material.tgz") + localFile, err := os.Create(localFileName) + if err != nil { + log.Fatalf("create file: %s", err) + } + defer localFile.Close() + + _, err = io.Copy(localFile, resp.Body) + if err != nil { + log.Fatalf("copy body to file: %s", err) + } + + log.Printf("Downloaded %s to %s", pkg, localFileName) +} diff --git a/build/generate-gitignores.go b/build/generate-options-gitignore.go similarity index 100% rename from build/generate-gitignores.go rename to build/generate-options-gitignore.go diff --git a/build/generate-licenses.go b/build/generate-options-license.go similarity index 100% rename from build/generate-licenses.go rename to build/generate-options-license.go diff --git a/modules/base/tool.go b/modules/base/tool.go index 71dcb83fb4850..283a2740b32d1 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -19,8 +19,6 @@ import ( "unicode" "unicode/utf8" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "github.com/dustin/go-humanize" @@ -200,28 +198,6 @@ func IsLetter(ch rune) bool { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch) } -// EntryIcon returns the octicon class for displaying files/directories -func EntryIcon(entry *git.TreeEntry) string { - switch { - case entry.IsLink(): - te, err := entry.FollowLink() - if err != nil { - log.Debug(err.Error()) - return "file-symlink-file" - } - if te.IsDir() { - return "file-directory-symlink" - } - return "file-symlink-file" - case entry.IsDir(): - return "file-directory-fill" - case entry.IsSubModule(): - return "file-submodule" - } - - return "file" -} - // SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value func SetupGiteaRoot() string { giteaRoot := os.Getenv("GITEA_ROOT") diff --git a/modules/fileicon/basic.go b/modules/fileicon/basic.go new file mode 100644 index 0000000000000..c66c5d70b0ab4 --- /dev/null +++ b/modules/fileicon/basic.go @@ -0,0 +1,33 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon + +import ( + "context" + "html/template" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/svg" +) + +func fileIconBasic(ctx context.Context, entry *git.TreeEntry) template.HTML { + svgName := "octicon-file" + switch { + case entry.IsLink(): + svgName = "octicon-file-symlink-file" + if te, err := entry.FollowLink(); err == nil && te.IsDir() { + svgName = "octicon-file-directory-symlink" + } + case entry.IsDir(): + svgName = "octicon-file-directory-fill" + case entry.IsSubModule(): + svgName = "octicon-file-submodule" + } + return svg.RenderHTML(svgName) +} + +func FileIcon(ctx context.Context, entry *git.TreeEntry) template.HTML { + // TODO: if it needs to use different file icon provider for different users, it could use ctx to check user setting and call fileIconBasic(ctx, entry) + return DefaultMaterialIconProvider().FileIcon(ctx, entry) +} diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go new file mode 100644 index 0000000000000..ba359ef7984ee --- /dev/null +++ b/modules/fileicon/material.go @@ -0,0 +1,201 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon + +import ( + "archive/tar" + "compress/gzip" + "context" + "html/template" + "io" + "net/http" + "path" + "strings" + "sync" + "time" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/options" + "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/util" +) + +type materialIconsData struct { + IconDefinitions map[string]*struct { + IconPath string `json:"iconPath"` + IconContent string `json:"-"` + } `json:"iconDefinitions"` + FileNames map[string]string `json:"fileNames"` + FolderNames map[string]string `json:"folderNames"` + FileExtensions map[string]string `json:"fileExtensions"` + LanguageIds map[string]string `json:"languageIds"` +} + +type MaterialIconProvider struct { + mu sync.RWMutex + + fs http.FileSystem + packFile string + packFileTime time.Time + lastStatTime time.Time + reloadInterval time.Duration + + materialIcons *materialIconsData +} + +var ( + materialIconProvider *MaterialIconProvider + materialIconProviderOnce sync.Once +) + +func DefaultMaterialIconProvider() *MaterialIconProvider { + materialIconProviderOnce.Do(func() { + materialIconProvider = NewMaterialIconProvider(options.AssetFS(), "fileicon/material.tgz") + }) + return materialIconProvider +} + +func NewMaterialIconProvider(fs http.FileSystem, packFile string) *MaterialIconProvider { + return &MaterialIconProvider{fs: fs, packFile: packFile, reloadInterval: time.Second} +} + +func (m *MaterialIconProvider) preprocessSvgContent(s string) string { + if !strings.HasPrefix(s, " m.reloadInterval { + m.lastStatTime = time.Now() + + f, err := m.fs.Open(m.packFile) + if err != nil { + log.Error("Failed to open material icon pack file: %v", err) + return + } + defer f.Close() + + fileInfo, err := f.Stat() + if err != nil { + log.Error("Failed to stat material icon pack file: %v", err) + return + } + if fileInfo.ModTime().Equal(m.packFileTime) { + return + } + + iconsData, err := m.loadDataFromPack(f) + if err != nil { + log.Error("Failed to load material icon pack file: %v", err) + return + } + m.materialIcons = iconsData + m.packFileTime = fileInfo.ModTime() + } +} + +func (m *MaterialIconProvider) FileIcon(ctx context.Context, entry *git.TreeEntry) template.HTML { + m.mu.RLock() + if time.Since(m.lastStatTime) > m.reloadInterval { + m.mu.RUnlock() + m.loadData() + m.mu.RLock() + } + defer m.mu.RUnlock() + + if m.materialIcons == nil { + return fileIconBasic(ctx, entry) + } + + if entry.IsLink() { + if te, err := entry.FollowLink(); err == nil && te.IsDir() { + return svg.RenderHTML("octicon-file-directory-symlink") // TODO: find some better icons for them + } + return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them + } + + name := m.findIconName(entry) + if iconDef, ok := m.materialIcons.IconDefinitions[name]; ok && iconDef.IconContent != "" { + return template.HTML(iconDef.IconContent) + } + return svg.RenderHTML("octicon-file") +} + +func (m *MaterialIconProvider) findIconName(entry *git.TreeEntry) string { + if entry.IsSubModule() { + return "folder-git" + } + + iconsData := m.materialIcons + fileName := path.Base(entry.Name()) + + if entry.IsDir() { + if s, ok := iconsData.FolderNames[fileName]; ok { + return s + } + if s, ok := iconsData.FolderNames[strings.ToLower(fileName)]; ok { + return s + } + return "folder" + } + + if s, ok := iconsData.FileNames[fileName]; ok { + return s + } + if s, ok := iconsData.FileNames[strings.ToLower(fileName)]; ok { + return s + } + + for i := len(fileName) - 1; i >= 0; i-- { + if fileName[i] == '.' { + ext := fileName[i+1:] + if s, ok := iconsData.FileExtensions[ext]; ok { + return s + } + } + } + + return "file" +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 2b918f42c099e..101f374e3001c 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -16,6 +16,7 @@ import ( system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/emoji" + "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" @@ -58,7 +59,7 @@ func NewFuncMap() template.FuncMap { "avatarByAction": AvatarByAction, "avatarByEmail": AvatarByEmail, "repoAvatar": RepoAvatar, - "EntryIcon": base.EntryIcon, + "FileIcon": fileicon.FileIcon, "MigrationIcon": MigrationIcon, "ActionIcon": ActionIcon, diff --git a/options/fileicon/material.tgz b/options/fileicon/material.tgz new file mode 100644 index 0000000000000..85d4d22c34c2b --- /dev/null +++ b/options/fileicon/material.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a915e28073f33e74d81ccfebf127c2264fa1dbfddc636106d0482ebe2a7bbf1a +size 300649 diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 3eabf9f181e57..1b7df40ff5070 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -50,8 +50,8 @@ + {{FileIcon $.Context $entry}} {{if $entry.IsSubModule}} - {{svg "octicon-file-submodule"}} {{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{/* FIXME: the usage of AppUrl seems incorrect, it would be fixed in the future, use AppSubUrl instead */}} {{if $refURL}} {{$entry.Name}}@{{ShortSha $subModuleFile.RefID}} @@ -61,7 +61,6 @@ {{else}} {{if $entry.IsDir}} {{$subJumpablePathName := $entry.GetSubJumpablePathName}} - {{svg "octicon-file-directory-fill"}} {{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}} {{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}} @@ -73,7 +72,6 @@ {{end}} {{else}} - {{svg (printf "octicon-%s" (EntryIcon $entry))}} {{$entry.Name}} {{end}} {{end}} From 4945914d94923ab5655984fb81ebe155bf12ca38 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 31 Jul 2023 14:49:41 +0800 Subject: [PATCH 2/3] normalizes SVG images when loading --- modules/fileicon/material.go | 11 +--- modules/html/html.go | 28 ++++----- modules/svg/processor.go | 59 +++++++++++++++++++ modules/svg/processor_test.go | 29 +++++++++ modules/svg/svg.go | 33 ++++------- .../img/svg/material-folder-generic.svg | 1 + .../img/svg/material-folder-symlink.svg | 1 + web_src/svg/material-folder-generic.svg | 1 + web_src/svg/material-folder-symlink.svg | 1 + 9 files changed, 117 insertions(+), 47 deletions(-) create mode 100644 modules/svg/processor.go create mode 100644 modules/svg/processor_test.go create mode 100644 public/assets/img/svg/material-folder-generic.svg create mode 100644 public/assets/img/svg/material-folder-symlink.svg create mode 100644 web_src/svg/material-folder-generic.svg create mode 100644 web_src/svg/material-folder-symlink.svg diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go index ba359ef7984ee..f329bf4be28d5 100644 --- a/modules/fileicon/material.go +++ b/modules/fileicon/material.go @@ -62,13 +62,6 @@ func NewMaterialIconProvider(fs http.FileSystem, packFile string) *MaterialIconP return &MaterialIconProvider{fs: fs, packFile: packFile, reloadInterval: time.Second} } -func (m *MaterialIconProvider) preprocessSvgContent(s string) string { - if !strings.HasPrefix(s, "= 1 { + if v, ok := others[0].(int); ok && v != 0 { + size = v + } } - class := defaultClass - if _class, ok := others[1].(string); ok && _class != "" { - if defaultClass == "" { - class = _class - } else { - class = defaultClass + " " + _class + if len(others) >= 2 { + if v, ok := others[1].(string); ok && v != "" { + if class != "" { + class += " " + } + class += v } } - return size, class } diff --git a/modules/svg/processor.go b/modules/svg/processor.go new file mode 100644 index 0000000000000..82248fb0c1216 --- /dev/null +++ b/modules/svg/processor.go @@ -0,0 +1,59 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package svg + +import ( + "bytes" + "fmt" + "regexp" + "sync" +) + +type normalizeVarsStruct struct { + reXMLDoc, + reComment, + reAttrXMLNs, + reAttrSize, + reAttrClassPrefix *regexp.Regexp +} + +var ( + normalizeVars *normalizeVarsStruct + normalizeVarsOnce sync.Once +) + +// Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes +// It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed. +func Normalize(data []byte, size int) []byte { + normalizeVarsOnce.Do(func() { + normalizeVars = &normalizeVarsStruct{ + reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), + reComment: regexp.MustCompile(`(?s)`), + + reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), + reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), + reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), + } + }) + data = normalizeVars.reXMLDoc.ReplaceAll(data, nil) + data = normalizeVars.reComment.ReplaceAll(data, nil) + + data = bytes.TrimSpace(data) + svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">")) + if !ok || !bytes.HasPrefix(svgTag, []byte(`') + normalized = append(normalized, svgRemaining...) + return normalized +} diff --git a/modules/svg/processor_test.go b/modules/svg/processor_test.go new file mode 100644 index 0000000000000..a0286666ed3f7 --- /dev/null +++ b/modules/svg/processor_test.go @@ -0,0 +1,29 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package svg + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalize(t *testing.T) { + res := Normalize([]byte("foo"), 1) + assert.Equal(t, "foo", string(res)) + + res = Normalize([]byte(` + +content`), 1) + assert.Equal(t, `content`, string(res)) + + res = Normalize([]byte(`content`), 16) + + assert.Equal(t, `content`, string(res)) +} diff --git a/modules/svg/svg.go b/modules/svg/svg.go index fc96ea8e6abdc..016e1dc08bb34 100644 --- a/modules/svg/svg.go +++ b/modules/svg/svg.go @@ -7,42 +7,35 @@ import ( "fmt" "html/template" "path" - "regexp" "strings" - "code.gitea.io/gitea/modules/html" + gitea_html "code.gitea.io/gitea/modules/html" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/public" ) -var ( - // SVGs contains discovered SVGs - SVGs = map[string]string{} - - widthRe = regexp.MustCompile(`width="[0-9]+?"`) - heightRe = regexp.MustCompile(`height="[0-9]+?"`) -) +var svgIcons map[string]string const defaultSize = 16 -// Init discovers SVGs and populates the `SVGs` variable +// Init discovers SVG icons and populates the `svgIcons` variable func Init() error { - files, err := public.AssetFS().ListFiles("assets/img/svg") + const svgAssetsPath = "assets/img/svg" + files, err := public.AssetFS().ListFiles(svgAssetsPath) if err != nil { return err } - // Remove `xmlns` because inline SVG does not need it - reXmlns := regexp.MustCompile(`(]*?)\s+xmlns="[^"]*"`) + svgIcons = make(map[string]string, len(files)) for _, file := range files { if path.Ext(file) != ".svg" { continue } - bs, err := public.AssetFS().ReadFile("assets/img/svg", file) + bs, err := public.AssetFS().ReadFile(svgAssetsPath, file) if err != nil { log.Error("Failed to read SVG file %s: %v", file, err) } else { - SVGs[file[:len(file)-4]] = reXmlns.ReplaceAllString(string(bs), "$1") + svgIcons[file[:len(file)-4]] = string(Normalize(bs, defaultSize)) } } return nil @@ -50,12 +43,12 @@ func Init() error { // RenderHTML renders icons - arguments icon name (string), size (int), class (string) func RenderHTML(icon string, others ...any) template.HTML { - size, class := html.ParseSizeAndClass(defaultSize, "", others...) - - if svgStr, ok := SVGs[icon]; ok { + 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 if size != defaultSize { - svgStr = widthRe.ReplaceAllString(svgStr, fmt.Sprintf(`width="%d"`, size)) - svgStr = heightRe.ReplaceAllString(svgStr, fmt.Sprintf(`height="%d"`, size)) + svgStr = strings.Replace(svgStr, fmt.Sprintf(`width="%d"`, defaultSize), fmt.Sprintf(`width="%d"`, size), 1) + svgStr = strings.Replace(svgStr, fmt.Sprintf(`height="%d"`, defaultSize), fmt.Sprintf(`height="%d"`, size), 1) } if class != "" { svgStr = strings.Replace(svgStr, `class="`, fmt.Sprintf(`class="%s `, class), 1) diff --git a/public/assets/img/svg/material-folder-generic.svg b/public/assets/img/svg/material-folder-generic.svg new file mode 100644 index 0000000000000..2eda47aec7ad4 --- /dev/null +++ b/public/assets/img/svg/material-folder-generic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/material-folder-symlink.svg b/public/assets/img/svg/material-folder-symlink.svg new file mode 100644 index 0000000000000..9dd5e1e7c5e3b --- /dev/null +++ b/public/assets/img/svg/material-folder-symlink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web_src/svg/material-folder-generic.svg b/web_src/svg/material-folder-generic.svg new file mode 100644 index 0000000000000..a6c6262c17066 --- /dev/null +++ b/web_src/svg/material-folder-generic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web_src/svg/material-folder-symlink.svg b/web_src/svg/material-folder-symlink.svg new file mode 100644 index 0000000000000..2db7bcd4de0d8 --- /dev/null +++ b/web_src/svg/material-folder-symlink.svg @@ -0,0 +1 @@ + \ No newline at end of file From 10b2d483635b8c6fc81b9eb578652e38bfeccb34 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 31 Jul 2023 15:03:35 +0800 Subject: [PATCH 3/3] add config option FILE_ICON_THEME, use builtin material-folder-generic for folder --- custom/conf/app.example.ini | 3 +++ modules/fileicon/basic.go | 7 +++++-- modules/fileicon/material.go | 6 +++++- modules/setting/ui.go | 2 ++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index e5f72d436c4b0..4d0658cf7d0b4 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1198,6 +1198,9 @@ LEVEL = Info ;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`. ;THEMES = auto,gitea,arc-green ;; +;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future. +;FILE_ICON_THEME = material +;; ;; All available reactions users can choose on issues/prs and comments. ;; Values can be emoji alias (:smile:) or a unicode emoji. ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png diff --git a/modules/fileicon/basic.go b/modules/fileicon/basic.go index c66c5d70b0ab4..531704066abe7 100644 --- a/modules/fileicon/basic.go +++ b/modules/fileicon/basic.go @@ -8,6 +8,7 @@ import ( "html/template" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" ) @@ -28,6 +29,8 @@ func fileIconBasic(ctx context.Context, entry *git.TreeEntry) template.HTML { } func FileIcon(ctx context.Context, entry *git.TreeEntry) template.HTML { - // TODO: if it needs to use different file icon provider for different users, it could use ctx to check user setting and call fileIconBasic(ctx, entry) - return DefaultMaterialIconProvider().FileIcon(ctx, entry) + if setting.UI.FileIconTheme == "material" { + return DefaultMaterialIconProvider().FileIcon(ctx, entry) + } + return fileIconBasic(ctx, entry) } diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go index f329bf4be28d5..1a85e40119531 100644 --- a/modules/fileicon/material.go +++ b/modules/fileicon/material.go @@ -144,12 +144,16 @@ func (m *MaterialIconProvider) FileIcon(ctx context.Context, entry *git.TreeEntr if entry.IsLink() { if te, err := entry.FollowLink(); err == nil && te.IsDir() { - return svg.RenderHTML("octicon-file-directory-symlink") // TODO: find some better icons for them + return svg.RenderHTML("material-folder-symlink") } return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them } name := m.findIconName(entry) + if name == "folder" { + // the material icon pack's "folder" icon doesn't look good, so use our built-in one + return svg.RenderHTML("material-folder-generic") + } if iconDef, ok := m.materialIcons.IconDefinitions[name]; ok && iconDef.IconContent != "" { return template.HTML(iconDef.IconContent) } diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 41438db40d5e7..e732e980ae82b 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -27,6 +27,7 @@ var UI = struct { DefaultShowFullName bool DefaultTheme string Themes []string + FileIconTheme string Reactions []string ReactionsLookup container.Set[string] `ini:"-"` CustomEmojis []string @@ -77,6 +78,7 @@ var UI = struct { ReactionMaxUserNum: 10, MaxDisplayFileSize: 8388608, DefaultTheme: `auto`, + FileIconTheme: `material`, Themes: []string{`auto`, `gitea`, `arc-green`}, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},