Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 6 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions build/generate-options-fileicon.go
Original file line number Diff line number Diff line change
@@ -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)
}
File renamed without changes.
File renamed without changes.
24 changes: 0 additions & 24 deletions modules/base/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
33 changes: 33 additions & 0 deletions modules/fileicon/basic.go
Original file line number Diff line number Diff line change
@@ -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)
}
201 changes: 201 additions & 0 deletions modules/fileicon/material.go
Original file line number Diff line number Diff line change
@@ -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, "<svg") {
return s
}
return `<svg class="svg svg-extpack-material" width="16" height="16" ` + s[4:]
}

func (m *MaterialIconProvider) loadDataFromPack(pack http.File) (*materialIconsData, error) {
gzf, err := gzip.NewReader(pack)
if err != nil {
return nil, err
}

files := map[string][]byte{}
tarReader := tar.NewReader(gzf)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
files[util.PathJoinRelX(header.Name)], err = io.ReadAll(tarReader)
if err != nil {
return nil, err
}
}

iconsData := materialIconsData{}
err = json.Unmarshal(files["package/dist/material-icons.json"], &iconsData)
if err != nil {
return nil, err
}

for name, icon := range iconsData.IconDefinitions {
iconContent := string(files[path.Join("package/dist", icon.IconPath)])
iconsData.IconDefinitions[name].IconContent = m.preprocessSvgContent(iconContent)
}

return &iconsData, nil
}

func (m *MaterialIconProvider) loadData() {
m.mu.Lock()
defer m.mu.Unlock()
if time.Since(m.lastStatTime) > 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"
}
3 changes: 2 additions & 1 deletion modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -58,7 +59,7 @@ func NewFuncMap() template.FuncMap {
"avatarByAction": AvatarByAction,
"avatarByEmail": AvatarByEmail,
"repoAvatar": RepoAvatar,
"EntryIcon": base.EntryIcon,
"FileIcon": fileicon.FileIcon,
"MigrationIcon": MigrationIcon,
"ActionIcon": ActionIcon,

Expand Down
3 changes: 3 additions & 0 deletions options/fileicon/material.tgz
Git LFS file not shown
Loading