Skip to content

Commit c502126

Browse files
committed
file icon
1 parent 4244ce0 commit c502126

File tree

11 files changed

+316
-37
lines changed

11 files changed

+316
-37
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
/web_src/fomantic/_site/globals/site.variables linguist-language=Less
99
/web_src/js/vendor/** -text -eol linguist-vendored
1010
Dockerfile.* linguist-language=Dockerfile
11+
options/fileicon/material.tgz filter=lfs diff=lfs merge=lfs -text

Makefile

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,8 @@ help:
232232
@echo " - fomantic build fomantic files"
233233
@echo " - generate run \"go generate\""
234234
@echo " - fmt format the Go code"
235-
@echo " - generate-license update license files"
236-
@echo " - generate-gitignore update gitignore files"
237235
@echo " - generate-manpage generate manpage"
236+
@echo " - generate-options generate licenses/gitignores/fileicons in options directory"
238237
@echo " - generate-swagger generate the swagger spec from code comments"
239238
@echo " - swagger-validate check if the swagger spec is valid"
240239
@echo " - go-licenses regenerate go licenses"
@@ -990,13 +989,11 @@ update-translations:
990989
mv ./translations/*.ini ./options/locale/
991990
rmdir ./translations
992991

993-
.PHONY: generate-license
994-
generate-license:
995-
$(GO) run build/generate-licenses.go
996-
997-
.PHONY: generate-gitignore
998-
generate-gitignore:
999-
$(GO) run build/generate-gitignores.go
992+
.PHONY: generate-options
993+
generate-options:
994+
$(GO) run build/generate-options-license.go
995+
$(GO) run build/generate-options-gitignore.go
996+
$(GO) run build/generate-options-fileicon.go
1000997

1001998
.PHONY: generate-images
1002999
generate-images: | node_modules

build/generate-options-fileicon.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//go:build ignore
2+
3+
package main
4+
5+
import (
6+
"encoding/json"
7+
"flag"
8+
"fmt"
9+
"io"
10+
"log"
11+
"net/http"
12+
"os"
13+
"path/filepath"
14+
)
15+
16+
func main() {
17+
var destination string
18+
flag.StringVar(&destination, "dest", "options/fileicon/", "destination for the fileicon")
19+
flag.Parse()
20+
21+
pkgName := "material-icon-theme"
22+
req, err := http.NewRequest("GET", fmt.Sprintf("https://registry.npmjs.org/%s/", pkgName), nil)
23+
if err != nil {
24+
log.Fatalf("http req: %s", err)
25+
}
26+
resp, err := http.DefaultClient.Do(req)
27+
if err != nil {
28+
log.Fatalf("http error: %s", err)
29+
}
30+
d := json.NewDecoder(resp.Body)
31+
defer resp.Body.Close()
32+
var m struct {
33+
DistTags map[string]string `json:"dist-tags"`
34+
}
35+
err = d.Decode(&m)
36+
if err != nil {
37+
log.Fatalf("json decode: %s", err)
38+
}
39+
40+
latestTag := m.DistTags["latest"]
41+
if latestTag == "" {
42+
log.Fatal("no latest tag")
43+
}
44+
45+
pkg := fmt.Sprintf("https://registry.npmjs.org/%s/-/%s-%s.tgz", pkgName, pkgName, latestTag)
46+
req, err = http.NewRequest("GET", pkg, nil)
47+
if err != nil {
48+
log.Fatalf("http req: %s", err)
49+
}
50+
resp, err = http.DefaultClient.Do(req)
51+
if err != nil {
52+
log.Fatalf("http error: %s", err)
53+
}
54+
defer resp.Body.Close()
55+
56+
localFileName := filepath.Join(destination, "material.tgz")
57+
localFile, err := os.Create(localFileName)
58+
if err != nil {
59+
log.Fatalf("create file: %s", err)
60+
}
61+
defer localFile.Close()
62+
63+
_, err = io.Copy(localFile, resp.Body)
64+
if err != nil {
65+
log.Fatalf("copy body to file: %s", err)
66+
}
67+
68+
log.Printf("Downloaded %s to %s", pkg, localFileName)
69+
}
File renamed without changes.
File renamed without changes.

modules/base/tool.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import (
1919
"unicode"
2020
"unicode/utf8"
2121

22-
"code.gitea.io/gitea/modules/git"
23-
"code.gitea.io/gitea/modules/log"
2422
"code.gitea.io/gitea/modules/setting"
2523

2624
"github.com/dustin/go-humanize"
@@ -200,28 +198,6 @@ func IsLetter(ch rune) bool {
200198
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch)
201199
}
202200

203-
// EntryIcon returns the octicon class for displaying files/directories
204-
func EntryIcon(entry *git.TreeEntry) string {
205-
switch {
206-
case entry.IsLink():
207-
te, err := entry.FollowLink()
208-
if err != nil {
209-
log.Debug(err.Error())
210-
return "file-symlink-file"
211-
}
212-
if te.IsDir() {
213-
return "file-directory-symlink"
214-
}
215-
return "file-symlink-file"
216-
case entry.IsDir():
217-
return "file-directory-fill"
218-
case entry.IsSubModule():
219-
return "file-submodule"
220-
}
221-
222-
return "file"
223-
}
224-
225201
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
226202
func SetupGiteaRoot() string {
227203
giteaRoot := os.Getenv("GITEA_ROOT")

modules/fileicon/basic.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2023 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+
}
29+
30+
func FileIcon(ctx context.Context, entry *git.TreeEntry) template.HTML {
31+
// 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)
32+
return DefaultMaterialIconProvider().FileIcon(ctx, entry)
33+
}

modules/fileicon/material.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package fileicon
5+
6+
import (
7+
"archive/tar"
8+
"compress/gzip"
9+
"context"
10+
"html/template"
11+
"io"
12+
"net/http"
13+
"path"
14+
"strings"
15+
"sync"
16+
"time"
17+
18+
"code.gitea.io/gitea/modules/git"
19+
"code.gitea.io/gitea/modules/json"
20+
"code.gitea.io/gitea/modules/log"
21+
"code.gitea.io/gitea/modules/options"
22+
"code.gitea.io/gitea/modules/svg"
23+
"code.gitea.io/gitea/modules/util"
24+
)
25+
26+
type materialIconsData struct {
27+
IconDefinitions map[string]*struct {
28+
IconPath string `json:"iconPath"`
29+
IconContent string `json:"-"`
30+
} `json:"iconDefinitions"`
31+
FileNames map[string]string `json:"fileNames"`
32+
FolderNames map[string]string `json:"folderNames"`
33+
FileExtensions map[string]string `json:"fileExtensions"`
34+
LanguageIds map[string]string `json:"languageIds"`
35+
}
36+
37+
type MaterialIconProvider struct {
38+
mu sync.RWMutex
39+
40+
fs http.FileSystem
41+
packFile string
42+
packFileTime time.Time
43+
lastStatTime time.Time
44+
reloadInterval time.Duration
45+
46+
materialIcons *materialIconsData
47+
}
48+
49+
var (
50+
materialIconProvider *MaterialIconProvider
51+
materialIconProviderOnce sync.Once
52+
)
53+
54+
func DefaultMaterialIconProvider() *MaterialIconProvider {
55+
materialIconProviderOnce.Do(func() {
56+
materialIconProvider = NewMaterialIconProvider(options.AssetFS(), "fileicon/material.tgz")
57+
})
58+
return materialIconProvider
59+
}
60+
61+
func NewMaterialIconProvider(fs http.FileSystem, packFile string) *MaterialIconProvider {
62+
return &MaterialIconProvider{fs: fs, packFile: packFile, reloadInterval: time.Second}
63+
}
64+
65+
func (m *MaterialIconProvider) preprocessSvgContent(s string) string {
66+
if !strings.HasPrefix(s, "<svg") {
67+
return s
68+
}
69+
return `<svg class="svg svg-extpack-material" width="16" height="16" ` + s[4:]
70+
}
71+
72+
func (m *MaterialIconProvider) loadDataFromPack(pack http.File) (*materialIconsData, error) {
73+
gzf, err := gzip.NewReader(pack)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
files := map[string][]byte{}
79+
tarReader := tar.NewReader(gzf)
80+
for {
81+
header, err := tarReader.Next()
82+
if err == io.EOF {
83+
break
84+
} else if err != nil {
85+
return nil, err
86+
}
87+
files[util.PathJoinRelX(header.Name)], err = io.ReadAll(tarReader)
88+
if err != nil {
89+
return nil, err
90+
}
91+
}
92+
93+
iconsData := materialIconsData{}
94+
err = json.Unmarshal(files["package/dist/material-icons.json"], &iconsData)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
for name, icon := range iconsData.IconDefinitions {
100+
iconContent := string(files[path.Join("package/dist", icon.IconPath)])
101+
iconsData.IconDefinitions[name].IconContent = m.preprocessSvgContent(iconContent)
102+
}
103+
104+
return &iconsData, nil
105+
}
106+
107+
func (m *MaterialIconProvider) loadData() {
108+
m.mu.Lock()
109+
defer m.mu.Unlock()
110+
if time.Since(m.lastStatTime) > m.reloadInterval {
111+
m.lastStatTime = time.Now()
112+
113+
f, err := m.fs.Open(m.packFile)
114+
if err != nil {
115+
log.Error("Failed to open material icon pack file: %v", err)
116+
return
117+
}
118+
defer f.Close()
119+
120+
fileInfo, err := f.Stat()
121+
if err != nil {
122+
log.Error("Failed to stat material icon pack file: %v", err)
123+
return
124+
}
125+
if fileInfo.ModTime().Equal(m.packFileTime) {
126+
return
127+
}
128+
129+
iconsData, err := m.loadDataFromPack(f)
130+
if err != nil {
131+
log.Error("Failed to load material icon pack file: %v", err)
132+
return
133+
}
134+
m.materialIcons = iconsData
135+
m.packFileTime = fileInfo.ModTime()
136+
}
137+
}
138+
139+
func (m *MaterialIconProvider) FileIcon(ctx context.Context, entry *git.TreeEntry) template.HTML {
140+
m.mu.RLock()
141+
if time.Since(m.lastStatTime) > m.reloadInterval {
142+
m.mu.RUnlock()
143+
m.loadData()
144+
m.mu.RLock()
145+
}
146+
defer m.mu.RUnlock()
147+
148+
if m.materialIcons == nil {
149+
return fileIconBasic(ctx, entry)
150+
}
151+
152+
if entry.IsLink() {
153+
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
154+
return svg.RenderHTML("octicon-file-directory-symlink") // TODO: find some better icons for them
155+
}
156+
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
157+
}
158+
159+
name := m.findIconName(entry)
160+
if iconDef, ok := m.materialIcons.IconDefinitions[name]; ok && iconDef.IconContent != "" {
161+
return template.HTML(iconDef.IconContent)
162+
}
163+
return svg.RenderHTML("octicon-file")
164+
}
165+
166+
func (m *MaterialIconProvider) findIconName(entry *git.TreeEntry) string {
167+
if entry.IsSubModule() {
168+
return "folder-git"
169+
}
170+
171+
iconsData := m.materialIcons
172+
fileName := path.Base(entry.Name())
173+
174+
if entry.IsDir() {
175+
if s, ok := iconsData.FolderNames[fileName]; ok {
176+
return s
177+
}
178+
if s, ok := iconsData.FolderNames[strings.ToLower(fileName)]; ok {
179+
return s
180+
}
181+
return "folder"
182+
}
183+
184+
if s, ok := iconsData.FileNames[fileName]; ok {
185+
return s
186+
}
187+
if s, ok := iconsData.FileNames[strings.ToLower(fileName)]; ok {
188+
return s
189+
}
190+
191+
for i := len(fileName) - 1; i >= 0; i-- {
192+
if fileName[i] == '.' {
193+
ext := fileName[i+1:]
194+
if s, ok := iconsData.FileExtensions[ext]; ok {
195+
return s
196+
}
197+
}
198+
}
199+
200+
return "file"
201+
}

modules/templates/helper.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
system_model "code.gitea.io/gitea/models/system"
1717
"code.gitea.io/gitea/modules/base"
1818
"code.gitea.io/gitea/modules/emoji"
19+
"code.gitea.io/gitea/modules/fileicon"
1920
"code.gitea.io/gitea/modules/markup"
2021
"code.gitea.io/gitea/modules/setting"
2122
"code.gitea.io/gitea/modules/svg"
@@ -58,7 +59,7 @@ func NewFuncMap() template.FuncMap {
5859
"avatarByAction": AvatarByAction,
5960
"avatarByEmail": AvatarByEmail,
6061
"repoAvatar": RepoAvatar,
61-
"EntryIcon": base.EntryIcon,
62+
"FileIcon": fileicon.FileIcon,
6263
"MigrationIcon": MigrationIcon,
6364
"ActionIcon": ActionIcon,
6465

options/fileicon/material.tgz

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:a915e28073f33e74d81ccfebf127c2264fa1dbfddc636106d0482ebe2a7bbf1a
3+
size 300649

0 commit comments

Comments
 (0)