aboutsummaryrefslogtreecommitdiffstats
path: root/modules/fileicon/material.go
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2025-03-10 15:57:17 +0800
committerGitHub <noreply@github.com>2025-03-10 15:57:17 +0800
commit34e5df6d300f92bc132df9fceca2f7fc65982c4c (patch)
treeea7125c0a8f901ef0fbc55382fa3d685d38309ec /modules/fileicon/material.go
parentae63568ce38f0b4248227c74d6872c1ff2bb06a7 (diff)
downloadgitea-34e5df6d300f92bc132df9fceca2f7fc65982c4c.tar.gz
gitea-34e5df6d300f92bc132df9fceca2f7fc65982c4c.zip
Add material icons for file list (#33837)
Diffstat (limited to 'modules/fileicon/material.go')
-rw-r--r--modules/fileicon/material.go150
1 files changed, 150 insertions, 0 deletions
diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go
new file mode 100644
index 0000000000..54666c76b2
--- /dev/null
+++ b/modules/fileicon/material.go
@@ -0,0 +1,150 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package fileicon
+
+import (
+ "html/template"
+ "path"
+ "strings"
+ "sync"
+
+ "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/reqctx"
+ "code.gitea.io/gitea/modules/svg"
+)
+
+type materialIconRulesData struct {
+ IconDefinitions map[string]*struct {
+ IconPath string `json:"iconPath"`
+ } `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 {
+ once sync.Once
+ rules *materialIconRulesData
+ svgs map[string]string
+}
+
+var materialIconProvider MaterialIconProvider
+
+func DefaultMaterialIconProvider() *MaterialIconProvider {
+ return &materialIconProvider
+}
+
+func (m *MaterialIconProvider) loadData() {
+ buf, err := options.AssetFS().ReadFile("fileicon/material-icon-rules.json")
+ if err != nil {
+ log.Error("Failed to read material icon rules: %v", err)
+ return
+ }
+ err = json.Unmarshal(buf, &m.rules)
+ if err != nil {
+ log.Error("Failed to unmarshal material icon rules: %v", err)
+ return
+ }
+
+ buf, err = options.AssetFS().ReadFile("fileicon/material-icon-svgs.json")
+ if err != nil {
+ log.Error("Failed to read material icon rules: %v", err)
+ return
+ }
+ err = json.Unmarshal(buf, &m.svgs)
+ if err != nil {
+ log.Error("Failed to unmarshal material icon rules: %v", err)
+ return
+ }
+ log.Debug("Loaded material icon rules and SVG images")
+}
+
+func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg string) template.HTML {
+ data := ctx.GetData()
+ renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool)
+ if renderedSVGs == nil {
+ renderedSVGs = make(map[string]bool)
+ data["_RenderedSVGs"] = renderedSVGs
+ }
+ // 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") {
+ panic("Invalid SVG icon")
+ }
+ svgID := "svg-mfi-" + name
+ svgCommonAttrs := `class="svg fileicon" 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>`)
+ }
+ svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]
+ renderedSVGs[svgID] = true
+ return template.HTML(svg)
+}
+
+func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
+ m.once.Do(m.loadData)
+
+ if m.rules == nil {
+ return BasicThemeIcon(entry)
+ }
+
+ if entry.IsLink() {
+ if te, err := entry.FollowLink(); err == nil && te.IsDir() {
+ 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 iconSVG, ok := m.svgs[name]; ok && iconSVG != "" {
+ return m.renderFileIconSVG(ctx, name, iconSVG)
+ }
+ return svg.RenderHTML("octicon-file")
+}
+
+func (m *MaterialIconProvider) findIconName(entry *git.TreeEntry) string {
+ if entry.IsSubModule() {
+ return "folder-git"
+ }
+
+ iconsData := m.rules
+ 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"
+}