From f7fbc6e0dec48feaae09a6e913b4a26aea4e7aed Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Mon, 22 Jun 2020 21:21:02 +0200 Subject: [PATCH] Show max 500 files per folder #364 No infinite scrolling in this case... Maybe we add it later. Waiting for user feedback. Signed-off-by: Michael Mayer --- frontend/src/model/folder.js | 2 ++ frontend/src/pages/library/files.vue | 14 +++++++----- internal/api/folder.go | 25 ++++++++++++++++----- internal/form/folder_search.go | 33 ++++++++++++++++++++++++++++ internal/query/files.go | 5 +++-- internal/query/files_test.go | 2 +- 6 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 internal/form/folder_search.go diff --git a/frontend/src/model/folder.js b/frontend/src/model/folder.js index 6c097051e..b0639dfd9 100644 --- a/frontend/src/model/folder.js +++ b/frontend/src/model/folder.js @@ -136,6 +136,8 @@ export class Folder extends RestModel { } response.models = []; + response.files = files.length; + response.folders = folders.length; response.count = count; response.limit = limit; response.offset = offset; diff --git a/frontend/src/pages/library/files.vue b/frontend/src/pages/library/files.vue index 35276fb33..710dca1f7 100644 --- a/frontend/src/pages/library/files.vue +++ b/frontend/src/pages/library/files.vue @@ -163,8 +163,8 @@ dirty: false, results: [], loading: true, - pageSize: 24, - offset: 0, + filesLimit: 500, + filesOffset: 0, page: 0, selection: [], settings: settings, @@ -360,6 +360,8 @@ const params = { files: true, uncached: true, + count: this.filesLimit, + offset: this.filesOffset, }; Object.assign(params, this.filter); @@ -387,7 +389,7 @@ Object.assign(this.lastFilter, this.filter); - this.offset = 0; + this.filesOffset = 0; this.page = 0; this.loading = true; this.listen = false; @@ -395,7 +397,7 @@ const params = this.searchParams(); Folder.originals(this.path, params).then(response => { - this.offset = this.pageSize; + this.filesOffset = this.filesLimit; this.results = response.models; this.breadcrumbs = this.getBreadcrumbs(); @@ -404,8 +406,10 @@ this.$notify.warn(this.$gettext('Directory is empty')); } else if (response.count === 1) { this.$notify.info(this.$gettext('One entry found')); - } else { + } else if (response.files < this.filesLimit) { this.$notify.info(response.count + this.$gettext(' entries found')); + } else { + this.$notify.warn(this.$gettext('Too many files in folder, showing first') + ` ${response.files}`); } }).finally(() => { this.dirty = false; diff --git a/internal/api/folder.go b/internal/api/folder.go index f63bd4368..608285d1b 100644 --- a/internal/api/folder.go +++ b/internal/api/folder.go @@ -9,10 +9,13 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/entity" + "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/service" + "github.com/photoprism/photoprism/pkg/txt" ) type FoldersResponse struct { @@ -31,11 +34,20 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName, return } + var f form.FolderSearch + start := time.Now() + err := c.MustBindWith(&f, binding.Form) + + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())}) + return + } + cache := service.Cache() - recursive := c.Query("recursive") != "" - listFiles := c.Query("files") != "" - uncached := listFiles || c.Query("uncached") != "" + recursive := f.Recursive + listFiles := f.Files + uncached := listFiles || f.Uncached resp := FoldersResponse{Root: rootName, Recursive: recursive, Cached: !uncached} path := c.Param("path") @@ -64,7 +76,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName, } if listFiles { - if files, err := query.FilesByPath(rootName, path); err != nil { + if files, err := query.FilesByPath(f.Count, f.Offset, rootName, path); err != nil { log.Errorf("folders: %s", err) } else { resp.Files = files @@ -78,8 +90,11 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName, } } + c.Header("X-Files", strconv.Itoa(len(resp.Files))) + c.Header("X-Folders", strconv.Itoa(len(resp.Folders))) c.Header("X-Count", strconv.Itoa(len(resp.Files)+len(resp.Folders))) - c.Header("X-Offset", "0") + c.Header("X-Limit", strconv.Itoa(f.Count)) + c.Header("X-Offset", strconv.Itoa(f.Offset)) c.JSON(http.StatusOK, resp) } diff --git a/internal/form/folder_search.go b/internal/form/folder_search.go new file mode 100644 index 000000000..79319d28d --- /dev/null +++ b/internal/form/folder_search.go @@ -0,0 +1,33 @@ +package form + +// FolderSearch represents search form fields for "/api/v1/folders". +type FolderSearch struct { + Query string `form:"q"` + Recursive bool `form:"recursive"` + Files bool `form:"files"` + Uncached bool `form:"uncached"` + Count int `form:"count" serialize:"-"` + Offset int `form:"offset" serialize:"-"` +} + +func (f *FolderSearch) GetQuery() string { + return f.Query +} + +func (f *FolderSearch) SetQuery(q string) { + f.Query = q +} + +func (f *FolderSearch) ParseQueryString() error { + return ParseQueryString(f) +} + +// Serialize returns a string containing non-empty fields and values of a struct. +func (f *FolderSearch) Serialize() string { + return Serialize(f, false) +} + +// SerializeAll returns a string containing all non-empty fields and values of a struct. +func (f *FolderSearch) SerializeAll() string { + return Serialize(f, true) +} diff --git a/internal/query/files.go b/internal/query/files.go index 44be03106..d5843e80e 100644 --- a/internal/query/files.go +++ b/internal/query/files.go @@ -7,7 +7,7 @@ import ( ) // FilesByPath returns a slice of files in a given originals folder. -func FilesByPath(rootName, pathName string) (files entity.Files, err error) { +func FilesByPath(limit, offset int, rootName, pathName string) (files entity.Files, err error) { if strings.HasPrefix(pathName, "/") { pathName = pathName[1:] } @@ -18,13 +18,14 @@ func FilesByPath(rootName, pathName string) (files entity.Files, err error) { Where("files.file_missing = 0"). Where("files.file_root = ? AND photos.photo_path = ?", rootName, pathName). Order("files.file_name"). + Limit(limit).Offset(offset). Find(&files).Error return files, err } // Files returns not-missing and not-deleted file entities in the range of limit and offset sorted by id. -func Files(limit int, offset int, pathName string, includeMissing bool) (files entity.Files, err error) { +func Files(limit, offset int, pathName string, includeMissing bool) (files entity.Files, err error) { if strings.HasPrefix(pathName, "/") { pathName = pathName[1:] } diff --git a/internal/query/files_test.go b/internal/query/files_test.go index 49c221d81..79fac38e0 100644 --- a/internal/query/files_test.go +++ b/internal/query/files_test.go @@ -9,7 +9,7 @@ import ( func TestFilesByPath(t *testing.T) { t.Run("files found", func(t *testing.T) { - files, err := FilesByPath(entity.RootOriginals, "2016/11") + files, err := FilesByPath(10, 0, entity.RootOriginals, "2016/11") t.Logf("files: %+v", files)