mirror of
https://github.com/go-gitea/gitea.git
synced 2025-05-05 23:42:54 +00:00
support the open-icon of folder (#34168)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
44d7d2973a
commit
2b76993415
@ -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
31
modules/fileicon/entry.go
Normal 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}
|
||||||
|
}
|
@ -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())
|
|
||||||
}
|
|
||||||
|
@ -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}))
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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":
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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" {
|
||||||
|
@ -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",
|
||||||
|
@ -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');
|
||||||
|
@ -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}}
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
@ -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: {},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user