mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-29 12:15:31 +00:00
Keep file tree view icons consistent with icon theme (#33921)
Fix #33914 before:  after:  --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
bcc38eb35f
commit
8c9d2bdee3
@ -13,7 +13,6 @@ import (
|
|||||||
"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/reqctx"
|
|
||||||
"code.gitea.io/gitea/modules/svg"
|
"code.gitea.io/gitea/modules/svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -62,13 +61,7 @@ func (m *MaterialIconProvider) loadData() {
|
|||||||
log.Debug("Loaded material icon rules and SVG images")
|
log.Debug("Loaded material icon rules and SVG images")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg, extraClass string) template.HTML {
|
func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg, extraClass 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.
|
// 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.
|
// Will try to refactor this in the future.
|
||||||
if !strings.HasPrefix(svg, "<svg") {
|
if !strings.HasPrefix(svg, "<svg") {
|
||||||
@ -76,16 +69,13 @@ func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name
|
|||||||
}
|
}
|
||||||
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"`
|
||||||
posOuterBefore := strings.IndexByte(svg, '>')
|
if p.IconSVGs[svgID] == "" {
|
||||||
if renderedSVGs[svgID] && posOuterBefore != -1 {
|
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
||||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
|
||||||
}
|
}
|
||||||
svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]
|
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
||||||
renderedSVGs[svgID] = true
|
|
||||||
return template.HTML(svg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML {
|
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||||
if m.rules == nil {
|
if m.rules == nil {
|
||||||
return BasicThemeIcon(entry)
|
return BasicThemeIcon(entry)
|
||||||
}
|
}
|
||||||
@ -110,7 +100,7 @@ func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.Tr
|
|||||||
case entry.IsSubModule():
|
case entry.IsSubModule():
|
||||||
extraClass = "octicon-file-submodule"
|
extraClass = "octicon-file-submodule"
|
||||||
}
|
}
|
||||||
return m.renderFileIconSVG(ctx, name, iconSVG, extraClass)
|
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
|
||||||
}
|
}
|
||||||
// TODO: use an interface or wrapper for git.Entry to make the code testable.
|
// TODO: use an interface or wrapper for git.Entry to make the code testable.
|
||||||
return BasicThemeIcon(entry)
|
return BasicThemeIcon(entry)
|
||||||
|
52
modules/fileicon/render.go
Normal file
52
modules/fileicon/render.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package fileicon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RenderedIconPool struct {
|
||||||
|
IconSVGs map[string]template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRenderedIconPool() *RenderedIconPool {
|
||||||
|
return &RenderedIconPool{
|
||||||
|
IconSVGs: make(map[string]template.HTML),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RenderedIconPool) RenderToHTML() template.HTML {
|
||||||
|
if len(p.IconSVGs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sb := &strings.Builder{}
|
||||||
|
sb.WriteString(`<div class=tw-hidden>`)
|
||||||
|
for _, icon := range p.IconSVGs {
|
||||||
|
sb.WriteString(string(icon))
|
||||||
|
}
|
||||||
|
sb.WriteString(`</div>`)
|
||||||
|
return template.HTML(sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
|
||||||
|
|
||||||
|
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||||
|
if setting.UI.FileIconTheme == "material" {
|
||||||
|
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
||||||
|
}
|
||||||
|
return BasicThemeIcon(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)
|
||||||
|
}
|
@ -32,19 +32,19 @@ func (err ErrNotExist) Unwrap() error {
|
|||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrBadLink entry.FollowLink error
|
// ErrSymlinkUnresolved entry.FollowLink error
|
||||||
type ErrBadLink struct {
|
type ErrSymlinkUnresolved struct {
|
||||||
Name string
|
Name string
|
||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrBadLink) Error() string {
|
func (err ErrSymlinkUnresolved) Error() string {
|
||||||
return fmt.Sprintf("%s: %s", err.Name, err.Message)
|
return fmt.Sprintf("%s: %s", err.Name, err.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrBadLink if some error is ErrBadLink
|
// IsErrSymlinkUnresolved if some error is ErrSymlinkUnresolved
|
||||||
func IsErrBadLink(err error) bool {
|
func IsErrSymlinkUnresolved(err error) bool {
|
||||||
_, ok := err.(ErrBadLink)
|
_, ok := err.(ErrSymlinkUnresolved)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type returns the type of the entry (commit, tree, blob)
|
// Type returns the type of the entry (commit, tree, blob)
|
||||||
@ -25,7 +27,7 @@ func (te *TreeEntry) Type() string {
|
|||||||
// FollowLink returns the entry pointed to by a symlink
|
// FollowLink returns the entry pointed to by a symlink
|
||||||
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
||||||
if !te.IsLink() {
|
if !te.IsLink() {
|
||||||
return nil, ErrBadLink{te.Name(), "not a symlink"}
|
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the link
|
// read the link
|
||||||
@ -56,13 +58,13 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return nil, ErrBadLink{te.Name(), "points outside of repo"}
|
return nil, ErrSymlinkUnresolved{te.Name(), "points outside of repo"}
|
||||||
}
|
}
|
||||||
|
|
||||||
target, err := t.GetTreeEntryByPath(lnk)
|
target, err := t.GetTreeEntryByPath(lnk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrNotExist(err) {
|
if IsErrNotExist(err) {
|
||||||
return nil, ErrBadLink{te.Name(), "broken link"}
|
return nil, ErrSymlinkUnresolved{te.Name(), "broken link"}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -70,33 +72,27 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FollowLinks returns the entry ultimately pointed to by a symlink
|
// FollowLinks returns the entry ultimately pointed to by a symlink
|
||||||
func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
|
func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) {
|
||||||
if !te.IsLink() {
|
if !te.IsLink() {
|
||||||
return nil, ErrBadLink{te.Name(), "not a symlink"}
|
return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"}
|
||||||
}
|
}
|
||||||
|
limit := util.OptionalArg(optLimit, 10)
|
||||||
entry := te
|
entry := te
|
||||||
for i := 0; i < 999; i++ {
|
for i := 0; i < limit; i++ {
|
||||||
if entry.IsLink() {
|
if !entry.IsLink() {
|
||||||
next, err := entry.FollowLink()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if next.ID == entry.ID {
|
|
||||||
return nil, ErrBadLink{
|
|
||||||
entry.Name(),
|
|
||||||
"recursive link",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry = next
|
|
||||||
} else {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
next, err := entry.FollowLink()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if next.ID == entry.ID {
|
||||||
|
return nil, ErrSymlinkUnresolved{entry.Name(), "recursive link"}
|
||||||
|
}
|
||||||
|
entry = next
|
||||||
}
|
}
|
||||||
if entry.IsLink() {
|
if entry.IsLink() {
|
||||||
return nil, ErrBadLink{
|
return nil, ErrSymlinkUnresolved{te.Name(), "too many levels of symbolic links"}
|
||||||
te.Name(),
|
|
||||||
"too many levels of symbolic links",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
@ -17,16 +17,12 @@ const (
|
|||||||
// EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of
|
// EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of
|
||||||
// added the base commit will not have the file in its tree so a mode of 0o000000 is used.
|
// added the base commit will not have the file in its tree so a mode of 0o000000 is used.
|
||||||
EntryModeNoEntry EntryMode = 0o000000
|
EntryModeNoEntry EntryMode = 0o000000
|
||||||
// EntryModeBlob
|
|
||||||
EntryModeBlob EntryMode = 0o100644
|
EntryModeBlob EntryMode = 0o100644
|
||||||
// EntryModeExec
|
EntryModeExec EntryMode = 0o100755
|
||||||
EntryModeExec EntryMode = 0o100755
|
|
||||||
// EntryModeSymlink
|
|
||||||
EntryModeSymlink EntryMode = 0o120000
|
EntryModeSymlink EntryMode = 0o120000
|
||||||
// EntryModeCommit
|
EntryModeCommit EntryMode = 0o160000
|
||||||
EntryModeCommit EntryMode = 0o160000
|
EntryModeTree EntryMode = 0o040000
|
||||||
// EntryModeTree
|
|
||||||
EntryModeTree EntryMode = 0o040000
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// String converts an EntryMode to a string
|
// String converts an EntryMode to a string
|
||||||
@ -34,12 +30,6 @@ func (e EntryMode) String() string {
|
|||||||
return strconv.FormatInt(int64(e), 8)
|
return strconv.FormatInt(int64(e), 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToEntryMode converts a string to an EntryMode
|
|
||||||
func ToEntryMode(value string) EntryMode {
|
|
||||||
v, _ := strconv.ParseInt(value, 8, 32)
|
|
||||||
return EntryMode(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseEntryMode(mode string) (EntryMode, error) {
|
func ParseEntryMode(mode string) (EntryMode, error) {
|
||||||
switch mode {
|
switch mode {
|
||||||
case "000000":
|
case "000000":
|
||||||
|
@ -15,8 +15,6 @@ import (
|
|||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"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/htmlutil"
|
"code.gitea.io/gitea/modules/htmlutil"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
@ -181,13 +179,6 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
|
|||||||
textColor, itemColor, itemHTML)
|
textColor, itemColor, itemHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ut *RenderUtils) RenderFileIcon(entry *git.TreeEntry) template.HTML {
|
|
||||||
if setting.UI.FileIconTheme == "material" {
|
|
||||||
return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, entry)
|
|
||||||
}
|
|
||||||
return fileicon.BasicThemeIcon(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderEmoji renders html text with emoji post processors
|
// RenderEmoji renders html text with emoji post processors
|
||||||
func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
|
func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
|
||||||
renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))
|
renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
pull_model "code.gitea.io/gitea/models/pull"
|
pull_model "code.gitea.io/gitea/models/pull"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
@ -87,10 +88,11 @@ func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TreeViewNodes(ctx *context.Context) {
|
func TreeViewNodes(ctx *context.Context) {
|
||||||
results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
|
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||||
|
results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetTreeViewNodes", err)
|
ctx.ServerError("GetTreeViewNodes", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results})
|
ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results, "renderedIconPool": renderedIconPool.IconSVGs})
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,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/lfs"
|
"code.gitea.io/gitea/modules/lfs"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@ -252,6 +253,16 @@ func LastCommit(ctx *context.Context) {
|
|||||||
ctx.HTML(http.StatusOK, tplRepoViewList)
|
ctx.HTML(http.StatusOK, tplRepoViewList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
|
||||||
|
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||||
|
fileIcons := map[string]template.HTML{}
|
||||||
|
for _, f := range files {
|
||||||
|
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry)
|
||||||
|
}
|
||||||
|
ctx.Data["FileIcons"] = fileIcons
|
||||||
|
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
||||||
|
}
|
||||||
|
|
||||||
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
|
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
|
||||||
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
|
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -293,6 +304,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx.Data["Files"] = files
|
ctx.Data["Files"] = files
|
||||||
|
prepareDirectoryFileIcons(ctx, files)
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if f.Commit == nil {
|
if f.Commit == nil {
|
||||||
ctx.Data["HasFilesWithoutLatestCommit"] = true
|
ctx.Data["HasFilesWithoutLatestCommit"] = true
|
||||||
|
@ -69,7 +69,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
|
|||||||
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
|
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
|
||||||
if entry.IsLink() {
|
if entry.IsLink() {
|
||||||
target, err := entry.FollowLinks()
|
target, err := entry.FollowLinks()
|
||||||
if err != nil && !git.IsErrBadLink(err) {
|
if err != nil && !git.IsErrSymlinkUnresolved(err) {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
||||||
readmeFiles[i] = entry
|
readmeFiles[i] = entry
|
||||||
|
@ -6,12 +6,14 @@ package files
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@ -140,8 +142,13 @@ func entryModeString(entryMode git.EntryMode) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TreeViewNode struct {
|
type TreeViewNode struct {
|
||||||
EntryName string `json:"entryName"`
|
EntryName string `json:"entryName"`
|
||||||
EntryMode string `json:"entryMode"`
|
EntryMode string `json:"entryMode"`
|
||||||
|
EntryIcon template.HTML `json:"entryIcon"`
|
||||||
|
EntryIconOpen template.HTML `json:"entryIconOpen,omitempty"`
|
||||||
|
|
||||||
|
SymLinkedToMode string `json:"symLinkedToMode,omitempty"` // TODO: for the EntryMode="symlink"
|
||||||
|
|
||||||
FullPath string `json:"fullPath"`
|
FullPath string `json:"fullPath"`
|
||||||
SubmoduleURL string `json:"submoduleUrl,omitempty"`
|
SubmoduleURL string `json:"submoduleUrl,omitempty"`
|
||||||
Children []*TreeViewNode `json:"children,omitempty"`
|
Children []*TreeViewNode `json:"children,omitempty"`
|
||||||
@ -151,13 +158,28 @@ func (node *TreeViewNode) sortLevel() int {
|
|||||||
return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1)
|
return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTreeViewNodeFromEntry(ctx context.Context, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
|
func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
|
||||||
node := &TreeViewNode{
|
node := &TreeViewNode{
|
||||||
EntryName: entry.Name(),
|
EntryName: entry.Name(),
|
||||||
EntryMode: entryModeString(entry.Mode()),
|
EntryMode: entryModeString(entry.Mode()),
|
||||||
FullPath: path.Join(parentDir, entry.Name()),
|
FullPath: path.Join(parentDir, entry.Name()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if entry.IsLink() {
|
||||||
|
// TODO: symlink to a folder or a file, the icon differs
|
||||||
|
target, err := entry.FollowLink()
|
||||||
|
if err == nil {
|
||||||
|
_ = target.IsDir()
|
||||||
|
// 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" {
|
||||||
if subModule, err := commit.GetSubModule(node.FullPath); err != nil {
|
if subModule, err := commit.GetSubModule(node.FullPath); err != nil {
|
||||||
log.Error("GetSubModule: %v", err)
|
log.Error("GetSubModule: %v", err)
|
||||||
@ -182,7 +204,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func listTreeNodes(ctx context.Context, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
|
func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||||
entries, err := tree.ListEntries()
|
entries, err := tree.ListEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -191,14 +213,14 @@ func listTreeNodes(ctx context.Context, commit *git.Commit, tree *git.Tree, tree
|
|||||||
subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/")
|
subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/")
|
||||||
nodes := make([]*TreeViewNode, 0, len(entries))
|
nodes := make([]*TreeViewNode, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
node := newTreeViewNodeFromEntry(ctx, commit, treePath, entry)
|
node := newTreeViewNodeFromEntry(ctx, renderedIconPool, commit, treePath, entry)
|
||||||
nodes = append(nodes, node)
|
nodes = append(nodes, node)
|
||||||
if entry.IsDir() && subPathDirName == entry.Name() {
|
if entry.IsDir() && subPathDirName == entry.Name() {
|
||||||
subTreePath := treePath + "/" + node.EntryName
|
subTreePath := treePath + "/" + node.EntryName
|
||||||
if subTreePath[0] == '/' {
|
if subTreePath[0] == '/' {
|
||||||
subTreePath = subTreePath[1:]
|
subTreePath = subTreePath[1:]
|
||||||
}
|
}
|
||||||
subNodes, err := listTreeNodes(ctx, commit, entry.Tree(), subTreePath, subPathRemaining)
|
subNodes, err := listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("listTreeNodes: %v", err)
|
log.Error("listTreeNodes: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@ -210,10 +232,10 @@ func listTreeNodes(ctx context.Context, commit *git.Commit, tree *git.Tree, tree
|
|||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTreeViewNodes(ctx context.Context, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
|
func GetTreeViewNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||||
entry, err := commit.GetTreeEntryByPath(treePath)
|
entry, err := commit.GetTreeEntryByPath(treePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return listTreeNodes(ctx, commit, entry.Tree(), treePath, subPath)
|
return listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), treePath, subPath)
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
@ -62,40 +64,51 @@ func TestGetTreeViewNodes(t *testing.T) {
|
|||||||
contexttest.LoadGitRepo(t, ctx)
|
contexttest.LoadGitRepo(t, ctx)
|
||||||
defer ctx.Repo.GitRepo.Close()
|
defer ctx.Repo.GitRepo.Close()
|
||||||
|
|
||||||
treeNodes, err := GetTreeViewNodes(ctx, ctx.Repo.Commit, "", "")
|
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||||
|
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>`)
|
||||||
|
}
|
||||||
|
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>`)
|
||||||
|
}
|
||||||
|
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`),
|
||||||
},
|
},
|
||||||
}, treeNodes)
|
}, treeNodes)
|
||||||
|
|
||||||
treeNodes, err = GetTreeViewNodes(ctx, ctx.Repo.Commit, "", "docs/README.md")
|
treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md")
|
||||||
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`),
|
||||||
Children: []*TreeViewNode{
|
Children: []*TreeViewNode{
|
||||||
{
|
{
|
||||||
EntryName: "README.md",
|
EntryName: "README.md",
|
||||||
EntryMode: "blob",
|
EntryMode: "blob",
|
||||||
FullPath: "docs/README.md",
|
FullPath: "docs/README.md",
|
||||||
|
EntryIcon: mockIconForFile(`svg-mfi-readme`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, treeNodes)
|
}, treeNodes)
|
||||||
|
|
||||||
treeNodes, err = GetTreeViewNodes(ctx, ctx.Repo.Commit, "docs", "README.md")
|
treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "docs", "README.md")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []*TreeViewNode{
|
assert.Equal(t, []*TreeViewNode{
|
||||||
{
|
{
|
||||||
EntryName: "README.md",
|
EntryName: "README.md",
|
||||||
EntryMode: "blob",
|
EntryMode: "blob",
|
||||||
FullPath: "docs/README.md",
|
FullPath: "docs/README.md",
|
||||||
|
EntryIcon: mockIconForFile(`svg-mfi-readme`),
|
||||||
},
|
},
|
||||||
}, treeNodes)
|
}, treeNodes)
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,14 @@
|
|||||||
{{svg "octicon-file-directory-fill"}} ..
|
{{svg "octicon-file-directory-fill"}} ..
|
||||||
</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}}
|
||||||
{{$commit := $item.Commit}}
|
{{$commit := $item.Commit}}
|
||||||
{{$submoduleFile := $item.SubmoduleFile}}
|
{{$submoduleFile := $item.SubmoduleFile}}
|
||||||
<div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}">
|
<div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}">
|
||||||
{{ctx.RenderUtils.RenderFileIcon $entry}}
|
{{index $.FileIcons $entry.Name}}
|
||||||
{{if $entry.IsSubModule}}
|
{{if $entry.IsSubModule}}
|
||||||
{{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}}
|
{{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}}
|
||||||
{{if $submoduleLink}}
|
{{if $submoduleLink}}
|
||||||
|
@ -3,6 +3,7 @@ import ViewFileTreeItem from './ViewFileTreeItem.vue';
|
|||||||
import {onMounted, ref} from 'vue';
|
import {onMounted, ref} from 'vue';
|
||||||
import {pathEscapeSegments} from '../utils/url.ts';
|
import {pathEscapeSegments} from '../utils/url.ts';
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
|
import {createElementFromHTML} from '../utils/dom.ts';
|
||||||
|
|
||||||
const elRoot = ref<HTMLElement | null>(null);
|
const elRoot = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
@ -18,6 +19,15 @@ const selectedItem = ref('');
|
|||||||
async function loadChildren(treePath: string, subPath: string = '') {
|
async function loadChildren(treePath: string, subPath: string = '') {
|
||||||
const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`);
|
const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
const poolSvgs = [];
|
||||||
|
for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) {
|
||||||
|
if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
|
||||||
|
}
|
||||||
|
if (poolSvgs.length) {
|
||||||
|
const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>');
|
||||||
|
svgContainer.innerHTML = poolSvgs.join('');
|
||||||
|
document.body.append(svgContainer);
|
||||||
|
}
|
||||||
return json.fileTreeNodes ?? null;
|
return json.fileTreeNodes ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import {ref} from 'vue';
|
|||||||
type Item = {
|
type Item = {
|
||||||
entryName: string;
|
entryName: string;
|
||||||
entryMode: string;
|
entryMode: string;
|
||||||
|
entryIcon: string;
|
||||||
|
entryIconOpen: string;
|
||||||
fullPath: string;
|
fullPath: string;
|
||||||
submoduleUrl?: string;
|
submoduleUrl?: string;
|
||||||
children?: Item[];
|
children?: Item[];
|
||||||
@ -80,7 +82,8 @@ const doGotoSubModule = () => {
|
|||||||
>
|
>
|
||||||
<!-- file -->
|
<!-- file -->
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
<SvgIcon name="octicon-file"/>
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span v-html="item.entryIcon"/>
|
||||||
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
|
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -92,11 +95,13 @@ const doGotoSubModule = () => {
|
|||||||
>
|
>
|
||||||
<!-- directory -->
|
<!-- directory -->
|
||||||
<div class="item-toggle">
|
<div class="item-toggle">
|
||||||
|
<!-- FIXME: use a general and global class for this animation -->
|
||||||
<SvgIcon v-if="isLoading" name="octicon-sync" class="job-status-rotate"/>
|
<SvgIcon v-if="isLoading" name="octicon-sync" class="job-status-rotate"/>
|
||||||
<SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
|
<SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
<SvgIcon class="text primary" :name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"/>
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span class="text primary" v-html="(!collapsed && item.entryIconOpen) ? item.entryIconOpen : item.entryIcon"/>
|
||||||
<span class="gt-ellipsis">{{ item.entryName }}</span>
|
<span class="gt-ellipsis">{{ item.entryName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user