support the open-icon of folder (#34168)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Kerwin Bryant 2025-04-29 10:51:32 +08:00 committed by GitHub
parent 44d7d2973a
commit 2b76993415
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 191 additions and 108 deletions

View File

@ -6,22 +6,26 @@ package fileicon
import ( import (
"html/template" "html/template"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/util"
) )
func BasicThemeIcon(entry *git.TreeEntry) template.HTML { func BasicEntryIconName(entry *EntryInfo) string {
svgName := "octicon-file" svgName := "octicon-file"
switch { switch {
case entry.IsLink(): case entry.EntryMode.IsLink():
svgName = "octicon-file-symlink-file" svgName = "octicon-file-symlink-file"
if te, err := entry.FollowLink(); err == nil && te.IsDir() { if entry.SymlinkToMode.IsDir() {
svgName = "octicon-file-directory-symlink" svgName = "octicon-file-directory-symlink"
} }
case entry.IsDir(): case entry.EntryMode.IsDir():
svgName = "octicon-file-directory-fill" svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill")
case entry.IsSubModule(): case entry.EntryMode.IsSubModule():
svgName = "octicon-file-submodule" svgName = "octicon-file-submodule"
} }
return svg.RenderHTML(svgName) return svgName
}
func BasicEntryIconHTML(entry *EntryInfo) template.HTML {
return svg.RenderHTML(BasicEntryIconName(entry))
} }

31
modules/fileicon/entry.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package fileicon
import "code.gitea.io/gitea/modules/git"
type EntryInfo struct {
FullName string
EntryMode git.EntryMode
SymlinkToMode git.EntryMode
IsOpen bool
}
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo {
ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
if gitEntry.IsLink() {
if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() {
ret.SymlinkToMode = te.Mode()
}
}
return ret
}
func EntryInfoFolder() *EntryInfo {
return &EntryInfo{EntryMode: git.EntryModeTree}
}
func EntryInfoFolderOpen() *EntryInfo {
return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true}
}

View File

@ -9,11 +9,12 @@ import (
"strings" "strings"
"sync" "sync"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/util"
) )
type materialIconRulesData struct { type materialIconRulesData struct {
@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
} }
svgID := "svg-mfi-" + name svgID := "svg-mfi-" + name
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"` svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
if p == nil {
return svgHTML
}
if p.IconSVGs[svgID] == "" { if p.IconSVGs[svgID] == "" {
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]) p.IconSVGs[svgID] = svgHTML
} }
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`) return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
} }
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML { func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
if m.rules == nil { if m.rules == nil {
return BasicThemeIcon(entry) return BasicEntryIconHTML(entry)
} }
if entry.IsLink() { if entry.EntryMode.IsLink() {
if te, err := entry.FollowLink(); err == nil && te.IsDir() { if entry.SymlinkToMode.IsDir() {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink") return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
} }
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
} }
name := m.findIconNameByGit(entry) name := m.FindIconName(entry)
// the material icon pack's "folder" icon doesn't look good, so use our built-in one iconSVG := m.svgs[name]
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work if iconSVG == "" {
if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" { name = "file"
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work if entry.EntryMode.IsDir() {
extraClass := "octicon-file" name = util.Iif(entry.IsOpen, "folder-open", "folder")
switch { }
case entry.IsDir(): iconSVG = m.svgs[name]
extraClass = "octicon-file-directory-fill" if iconSVG == "" {
case entry.IsSubModule(): setting.PanicInDevOrTesting("missing file icon for %s", name)
extraClass = "octicon-file-submodule"
} }
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
} }
// TODO: use an interface or wrapper for git.Entry to make the code testable.
return BasicThemeIcon(entry) // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
extraClass := "octicon-file"
switch {
case entry.EntryMode.IsDir():
extraClass = BasicEntryIconName(entry)
case entry.EntryMode.IsSubModule():
extraClass = "octicon-file-submodule"
}
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
} }
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
return "" return ""
} }
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
fileNameLower := strings.ToLower(path.Base(name)) if entry.EntryMode.IsSubModule() {
if isDir { return "folder-git"
}
fileNameLower := strings.ToLower(path.Base(entry.FullName))
if entry.EntryMode.IsDir() {
if s, ok := m.rules.FolderNames[fileNameLower]; ok { if s, ok := m.rules.FolderNames[fileNameLower]; ok {
return s return s
} }
return "folder" return util.Iif(entry.IsOpen, "folder-open", "folder")
} }
if s, ok := m.rules.FileNames[fileNameLower]; ok { if s, ok := m.rules.FileNames[fileNameLower]; ok {
@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
return "file" return "file"
} }
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
if entry.IsSubModule() {
return "folder-git"
}
return m.FindIconName(entry.Name(), entry.IsDir())
}

View File

@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -19,8 +20,8 @@ func TestMain(m *testing.M) {
func TestFindIconName(t *testing.T) { func TestFindIconName(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
p := fileicon.DefaultMaterialIconProvider() p := fileicon.DefaultMaterialIconProvider()
assert.Equal(t, "php", p.FindIconName("foo.php", false)) assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
assert.Equal(t, "php", p.FindIconName("foo.PHP", false)) assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
assert.Equal(t, "javascript", p.FindIconName("foo.js", false)) assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false)) assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
} }

View File

@ -7,7 +7,6 @@ import (
"html/template" "html/template"
"strings" "strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
return template.HTML(sb.String()) return template.HTML(sb.String())
} }
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
if setting.UI.FileIconTheme == "material" { if setting.UI.FileIconTheme == "material" {
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
} }
return BasicThemeIcon(entry) return BasicEntryIconHTML(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)
} }

View File

@ -30,6 +30,31 @@ func (e EntryMode) String() string {
return strconv.FormatInt(int64(e), 8) return strconv.FormatInt(int64(e), 8)
} }
// IsSubModule if the entry is a sub module
func (e EntryMode) IsSubModule() bool {
return e == EntryModeCommit
}
// IsDir if the entry is a sub dir
func (e EntryMode) IsDir() bool {
return e == EntryModeTree
}
// IsLink if the entry is a symlink
func (e EntryMode) IsLink() bool {
return e == EntryModeSymlink
}
// IsRegular if the entry is a regular file
func (e EntryMode) IsRegular() bool {
return e == EntryModeBlob
}
// IsExecutable if the entry is an executable file (not necessarily binary)
func (e EntryMode) IsExecutable() bool {
return e == EntryModeExec
}
func ParseEntryMode(mode string) (EntryMode, error) { func ParseEntryMode(mode string) (EntryMode, error) {
switch mode { switch mode {
case "000000": case "000000":

View File

@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
// IsSubModule if the entry is a sub module // IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool { func (te *TreeEntry) IsSubModule() bool {
return te.entryMode == EntryModeCommit return te.entryMode.IsSubModule()
} }
// IsDir if the entry is a sub dir // IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool { func (te *TreeEntry) IsDir() bool {
return te.entryMode == EntryModeTree return te.entryMode.IsDir()
} }
// IsLink if the entry is a symlink // IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool { func (te *TreeEntry) IsLink() bool {
return te.entryMode == EntryModeSymlink return te.entryMode.IsLink()
} }
// IsRegular if the entry is a regular file // IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool { func (te *TreeEntry) IsRegular() bool {
return te.entryMode == EntryModeBlob return te.entryMode.IsRegular()
} }
// IsExecutable if the entry is an executable file (not necessarily binary) // IsExecutable if the entry is an executable file (not necessarily binary)
func (te *TreeEntry) IsExecutable() bool { func (te *TreeEntry) IsExecutable() bool {
return te.entryMode == EntryModeExec return te.entryMode.IsExecutable()
} }
// Blob returns the blob object the entry // Blob returns the blob object the entry

View File

@ -21,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -369,7 +370,11 @@ func Diff(ctx *context.Context) {
return return
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)

View File

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
csv_module "code.gitea.io/gitea/modules/csv" csv_module "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -639,7 +640,11 @@ func PrepareCompareDiff(
return false return false
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)

View File

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
issue_template "code.gitea.io/gitea/modules/issue/template" issue_template "code.gitea.io/gitea/modules/issue/template"
@ -823,7 +824,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if reviewState != nil { if reviewState != nil {
filesViewedState = reviewState.UpdatedFiles filesViewedState = reviewState.UpdatedFiles
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState)
renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState)
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
ctx.Data["Diff"] = diff ctx.Data["Diff"] = diff

View File

@ -4,6 +4,7 @@
package repo package repo
import ( import (
"html/template"
"net/http" "net/http"
"strings" "strings"
@ -67,7 +68,7 @@ type WebDiffFileItem struct {
EntryMode string EntryMode string
IsViewed bool IsViewed bool
Children []*WebDiffFileItem Children []*WebDiffFileItem
// TODO: add icon support in the future FileIcon template.HTML
} }
// WebDiffFileTree is used by frontend, check the field names in frontend before changing // WebDiffFileTree is used by frontend, check the field names in frontend before changing
@ -77,7 +78,7 @@ type WebDiffFileTree struct {
// transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering // transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering
// it also takes a map of file names to their viewed state, which is used to mark files as viewed // it also takes a map of file names to their viewed state, which is used to mark files as viewed
func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) { func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot} dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
addItem := func(item *WebDiffFileItem) { addItem := func(item *WebDiffFileItem) {
var parentPath string var parentPath string
@ -110,6 +111,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status} item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
item.NameHash = git.HashFilePathForWebUI(item.FullName) item.NameHash = git.HashFilePathForWebUI(item.FullName)
item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode})
switch file.HeadMode { switch file.HeadMode {
case git.EntryModeTree: case git.EntryModeTree:

View File

@ -4,9 +4,11 @@
package repo package repo
import ( import (
"html/template"
"testing" "testing"
pull_model "code.gitea.io/gitea/models/pull" pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
@ -14,7 +16,8 @@ import (
) )
func TestTransformDiffTreeForWeb(t *testing.T) { func TestTransformDiffTreeForWeb(t *testing.T) {
ret := transformDiffTreeForWeb(&gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{ renderedIconPool := fileicon.NewRenderedIconPool()
ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
{ {
Status: "changed", Status: "changed",
HeadPath: "dir-a/dir-a-x/file-deep", HeadPath: "dir-a/dir-a-x/file-deep",
@ -29,6 +32,9 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
"dir-a/dir-a-x/file-deep": pull_model.Viewed, "dir-a/dir-a-x/file-deep": pull_model.Viewed,
}) })
mockIconForFile := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
}
assert.Equal(t, WebDiffFileTree{ assert.Equal(t, WebDiffFileTree{
TreeRoot: WebDiffFileItem{ TreeRoot: WebDiffFileItem{
Children: []*WebDiffFileItem{ Children: []*WebDiffFileItem{
@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b", NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
DiffStatus: "changed", DiffStatus: "changed",
IsViewed: true, IsViewed: true,
FileIcon: mockIconForFile(`svg-mfi-file`),
}, },
}, },
}, },
@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
FullName: "file1", FullName: "file1",
NameHash: "60b27f004e454aca81b0480209cce5081ec52390", NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
DiffStatus: "added", DiffStatus: "added",
FileIcon: mockIconForFile(`svg-mfi-file`),
}, },
}, },
}, },

View File

@ -257,8 +257,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
renderedIconPool := fileicon.NewRenderedIconPool() renderedIconPool := fileicon.NewRenderedIconPool()
fileIcons := map[string]template.HTML{} fileIcons := map[string]template.HTML{}
for _, f := range files { for _, f := range files {
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry) fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry))
} }
fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.Data["FileIcons"] = fileIcons ctx.Data["FileIcons"] = fileIcons
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }

View File

@ -165,19 +165,11 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re
FullPath: path.Join(parentDir, entry.Name()), FullPath: path.Join(parentDir, entry.Name()),
} }
if entry.IsLink() { entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry)
// TODO: symlink to a folder or a file, the icon differs node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
target, err := entry.FollowLink() if entryInfo.EntryMode.IsDir() {
if err == nil { entryInfo.IsOpen = true
_ = target.IsDir() node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
// if target.IsDir() { } else { }
}
}
if node.EntryIcon == "" {
node.EntryIcon = fileicon.RenderEntryIcon(renderedIconPool, entry)
// TODO: no open icon support yet
// node.EntryIconOpen = fileicon.RenderEntryIconOpen(renderedIconPool, entry)
} }
if node.EntryMode == "commit" { if node.EntryMode == "commit" {

View File

@ -71,14 +71,18 @@ func TestGetTreeViewNodes(t *testing.T) {
mockIconForFolder := func(id string) template.HTML { mockIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`) return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
} }
mockOpenIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
}
treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "") treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {
EntryName: "docs", EntryName: "docs",
EntryMode: "tree", EntryMode: "tree",
FullPath: "docs", FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
}, },
}, treeNodes) }, treeNodes)
@ -86,10 +90,11 @@ func TestGetTreeViewNodes(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {
EntryName: "docs", EntryName: "docs",
EntryMode: "tree", EntryMode: "tree",
FullPath: "docs", FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
Children: []*TreeViewNode{ Children: []*TreeViewNode{
{ {
EntryName: "README.md", EntryName: "README.md",

View File

@ -60,6 +60,7 @@
{{end}} {{end}}
<div id="diff-container"> <div id="diff-container">
{{if $showFileTree}} {{if $showFileTree}}
{{$.FileIconPoolHTML}}
<div id="diff-file-tree" class="tw-hidden not-mobile"></div> <div id="diff-file-tree" class="tw-hidden not-mobile"></div>
<script> <script>
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden'); if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');

View File

@ -4,12 +4,12 @@
{{template "repo/latest_commit" .}} {{template "repo/latest_commit" .}}
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div> <div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
</div> </div>
{{$.FileIconPoolHTML}}
{{if .HasParentPath}} {{if .HasParentPath}}
<a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}"> <a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}">
{{svg "octicon-file-directory-fill"}} .. {{index $.FileIcons ".."}} ..
</a> </a>
{{end}} {{end}}
{{$.FileIconPoolHTML}}
{{range $item := .Files}} {{range $item := .Files}}
<div class="repo-file-item"> <div class="repo-file-item">
{{$entry := $item.Entry}} {{$entry := $item.Entry}}

View File

@ -22,13 +22,6 @@ function getIconForDiffStatus(pType: DiffStatus) {
}; };
return diffTypes[pType] ?? diffTypes['']; return diffTypes[pType] ?? diffTypes[''];
} }
function entryIcon(entry: DiffTreeEntry) {
if (entry.EntryMode === 'commit') {
return 'octicon-file-submodule';
}
return 'octicon-file';
}
</script> </script>
<template> <template>
@ -36,10 +29,8 @@ function entryIcon(entry: DiffTreeEntry) {
<div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed"> <div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed">
<!-- directory --> <!-- directory -->
<SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/> <SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
<SvgIcon <!-- eslint-disable-next-line vue/no-v-html -->
class="text primary" <span class="tw-contents" v-html="collapsed ? store.folderIcon : store.folderOpenIcon"/>
:name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"
/>
<span class="gt-ellipsis">{{ item.DisplayName }}</span> <span class="gt-ellipsis">{{ item.DisplayName }}</span>
</div> </div>
@ -53,7 +44,8 @@ function entryIcon(entry: DiffTreeEntry) {
:title="item.DisplayName" :href="'#diff-' + item.NameHash" :title="item.DisplayName" :href="'#diff-' + item.NameHash"
> >
<!-- file --> <!-- file -->
<SvgIcon :name="entryIcon(item)"/> <!-- eslint-disable-next-line vue/no-v-html -->
<span class="tw-contents" v-html="item.FileIcon"/>
<span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span> <span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span>
<SvgIcon <SvgIcon
:name="getIconForDiffStatus(item.DiffStatus).name" :name="getIconForDiffStatus(item.DiffStatus).name"

View File

@ -9,6 +9,7 @@ test('diff-tree', () => {
'IsViewed': false, 'IsViewed': false,
'NameHash': '....', 'NameHash': '....',
'DiffStatus': '', 'DiffStatus': '',
'FileIcon': '',
'Children': [ 'Children': [
{ {
'FullName': 'dir1', 'FullName': 'dir1',
@ -17,6 +18,7 @@ test('diff-tree', () => {
'IsViewed': false, 'IsViewed': false,
'NameHash': '....', 'NameHash': '....',
'DiffStatus': '', 'DiffStatus': '',
'FileIcon': '',
'Children': [ 'Children': [
{ {
'FullName': 'dir1/test.txt', 'FullName': 'dir1/test.txt',
@ -25,6 +27,7 @@ test('diff-tree', () => {
'NameHash': '....', 'NameHash': '....',
'EntryMode': '', 'EntryMode': '',
'IsViewed': false, 'IsViewed': false,
'FileIcon': '',
'Children': null, 'Children': null,
}, },
], ],
@ -36,11 +39,12 @@ test('diff-tree', () => {
'DiffStatus': 'added', 'DiffStatus': 'added',
'EntryMode': '', 'EntryMode': '',
'IsViewed': false, 'IsViewed': false,
'FileIcon': '',
'Children': null, 'Children': null,
}, },
], ],
}, },
}); }, '', '');
diffTreeStoreSetViewed(store, 'dir1/test.txt', true); diffTreeStoreSetViewed(store, 'dir1/test.txt', true);
expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true); expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true);
expect(store.fullNameMap['dir1'].IsViewed).toBe(true); expect(store.fullNameMap['dir1'].IsViewed).toBe(true);

View File

@ -13,7 +13,7 @@ export type DiffTreeEntry = {
EntryMode: string, EntryMode: string,
IsViewed: boolean, IsViewed: boolean,
Children: DiffTreeEntry[], Children: DiffTreeEntry[],
FileIcon: string,
ParentEntry?: DiffTreeEntry, ParentEntry?: DiffTreeEntry,
} }
@ -22,6 +22,8 @@ type DiffFileTreeData = {
}; };
type DiffFileTree = { type DiffFileTree = {
folderIcon: string;
folderOpenIcon: string;
diffFileTree: DiffFileTreeData; diffFileTree: DiffFileTreeData;
fullNameMap?: Record<string, DiffTreeEntry> fullNameMap?: Record<string, DiffTreeEntry>
fileTreeIsVisible: boolean; fileTreeIsVisible: boolean;
@ -31,7 +33,7 @@ type DiffFileTree = {
let diffTreeStoreReactive: Reactive<DiffFileTree>; let diffTreeStoreReactive: Reactive<DiffFileTree>;
export function diffTreeStore() { export function diffTreeStore() {
if (!diffTreeStoreReactive) { if (!diffTreeStoreReactive) {
diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree); diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree, pageData.FolderIcon, pageData.FolderOpenIcon);
} }
return diffTreeStoreReactive; return diffTreeStoreReactive;
} }
@ -55,9 +57,11 @@ function fillFullNameMap(map: Record<string, DiffTreeEntry>, entry: DiffTreeEntr
} }
} }
export function reactiveDiffTreeStore(data: DiffFileTreeData): Reactive<DiffFileTree> { export function reactiveDiffTreeStore(data: DiffFileTreeData, folderIcon: string, folderOpenIcon: string): Reactive<DiffFileTree> {
const store = reactive({ const store = reactive({
diffFileTree: data, diffFileTree: data,
folderIcon,
folderOpenIcon,
fileTreeIsVisible: false, fileTreeIsVisible: false,
selectedItem: '', selectedItem: '',
fullNameMap: {}, fullNameMap: {},