Fix indexer and add sort by file name #328

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-05-22 19:05:16 +02:00
parent a7122ff4e1
commit e796d036c2
17 changed files with 113 additions and 21 deletions

View file

@ -13,5 +13,10 @@
"display": "standalone", "display": "standalone",
"orientation": "portrait", "orientation": "portrait",
"theme_color": "#333333", "theme_color": "#333333",
"background_color": "#333333" "background_color": "#333333",
"permissions": [
"geolocation",
"downloads",
"storage"
]
} }

View file

@ -154,6 +154,7 @@
{value: 'imported', text: this.$gettext('Recently added')}, {value: 'imported', text: this.$gettext('Recently added')},
{value: 'newest', text: this.$gettext('Newest first')}, {value: 'newest', text: this.$gettext('Newest first')},
{value: 'oldest', text: this.$gettext('Oldest first')}, {value: 'oldest', text: this.$gettext('Oldest first')},
{value: 'name', text: this.$gettext('Sort by file name')},
{value: 'similar', text: this.$gettext('Group by similarity')}, {value: 'similar', text: this.$gettext('Group by similarity')},
{value: 'relevance', text: this.$gettext('Most relevant')}, {value: 'relevance', text: this.$gettext('Most relevant')},
], ],

View file

@ -136,6 +136,14 @@
{{ photo.getPhotoInfo() }} {{ photo.getPhotoInfo() }}
</button> </button>
</template> </template>
<template v-if="filter.order === 'name' && $config.feature('download')">
<br/>
<button @click.exact="downloadFile(index)"
title="Name">
<v-icon size="14">save</v-icon>
{{ photo.FileName }}
</button>
</template>
<template v-if="showLocation && photo.LocationID"> <template v-if="showLocation && photo.LocationID">
<br/> <br/>
<button @click.exact="openLocation(index)" title="Location"> <button @click.exact="openLocation(index)" title="Location">
@ -162,6 +170,7 @@
editPhoto: Function, editPhoto: Function,
openLocation: Function, openLocation: Function,
album: Object, album: Object,
filter: Object,
}, },
data() { data() {
return { return {
@ -175,6 +184,13 @@
}; };
}, },
methods: { methods: {
downloadFile(index) {
const photo = this.photos[index];
const link = document.createElement('a')
link.href = "/api/v1/download/" + photo.FileHash;
link.download = photo.FileName;
link.click()
},
onSelect(ev, index) { onSelect(ev, index) {
if (ev.shiftKey) { if (ev.shiftKey) {
this.selectRange(index); this.selectRange(index);

View file

@ -53,7 +53,11 @@
</button> </button>
</td> </td>
<td class="p-photo-desc hidden-xs-only"> <td class="p-photo-desc hidden-xs-only">
<button v-if="props.item.LocationID && showLocation" @click.stop.prevent="openLocation(props.index)" <button @click.exact="downloadFile(props.index)"
title="Name" v-if="filter.order === 'name'">
{{ props.item.FileName }}
</button>
<button v-else-if="props.item.LocationID && showLocation" @click.stop.prevent="openLocation(props.index)"
style="user-select: none;"> style="user-select: none;">
{{ props.item.getLocation() }} {{ props.item.getLocation() }}
</button> </button>
@ -86,6 +90,7 @@
editPhoto: Function, editPhoto: Function,
openLocation: Function, openLocation: Function,
album: Object, album: Object,
filter: Object,
}, },
data() { data() {
let m = this.$gettext('Try using other terms and search options such as category, country and camera.'); let m = this.$gettext('Try using other terms and search options such as category, country and camera.');
@ -94,6 +99,8 @@
m += " " + this.$gettext("Non-photographic and low-quality images require a review before they appear in search results."); m += " " + this.$gettext("Non-photographic and low-quality images require a review before they appear in search results.");
} }
let showName = this.filter.order === 'name'
return { return {
notFoundMessage: m, notFoundMessage: m,
'selected': [], 'selected': [],
@ -102,9 +109,10 @@
{text: this.$gettext('Title'), value: 'PhotoTitle'}, {text: this.$gettext('Title'), value: 'PhotoTitle'},
{text: this.$gettext('Taken'), class: 'hidden-xs-only', value: 'TakenAt'}, {text: this.$gettext('Taken'), class: 'hidden-xs-only', value: 'TakenAt'},
{text: this.$gettext('Camera'), class: 'hidden-sm-and-down', value: 'CameraModel'}, {text: this.$gettext('Camera'), class: 'hidden-sm-and-down', value: 'CameraModel'},
{text: this.$gettext('Location'), class: 'hidden-xs-only', value: 'LocLabel'}, {text: showName ? this.$gettext('Name') : this.$gettext('Location'), class: 'hidden-xs-only', value: showName ? 'FileName' : 'LocLabel'},
{text: '', value: '', sortable: false, align: 'center'}, {text: '', value: '', sortable: false, align: 'center'},
], ],
showName: showName,
showLocation: this.$config.settings().features.places, showLocation: this.$config.settings().features.places,
hidePrivate: this.$config.settings().features.private, hidePrivate: this.$config.settings().features.private,
mouseDown: { mouseDown: {
@ -128,6 +136,13 @@
}, },
}, },
methods: { methods: {
downloadFile(index) {
const photo = this.photos[index];
const link = document.createElement('a')
link.href = "/api/v1/download/" + photo.FileHash;
link.download = photo.FileName;
link.click()
},
onSelect(ev, index) { onSelect(ev, index) {
if (ev.shiftKey) { if (ev.shiftKey) {
this.selectRange(index); this.selectRange(index);

View file

@ -122,6 +122,7 @@
openPhoto: Function, openPhoto: Function,
editPhoto: Function, editPhoto: Function,
album: Object, album: Object,
filter: Object,
}, },
data() { data() {
return { return {

View file

@ -170,6 +170,7 @@
{value: 'imported', text: this.$gettext('Recently added')}, {value: 'imported', text: this.$gettext('Recently added')},
{value: 'newest', text: this.$gettext('Newest first')}, {value: 'newest', text: this.$gettext('Newest first')},
{value: 'oldest', text: this.$gettext('Oldest first')}, {value: 'oldest', text: this.$gettext('Oldest first')},
{value: 'name', text: this.$gettext('Sort by file name')},
{value: 'similar', text: this.$gettext('Group by similarity')}, {value: 'similar', text: this.$gettext('Group by similarity')},
{value: 'relevance', text: this.$gettext('Most relevant')}, {value: 'relevance', text: this.$gettext('Most relevant')},
], ],

View file

@ -24,6 +24,7 @@ export class Photo extends RestModel {
TimeZone: "", TimeZone: "",
PhotoPath: "", PhotoPath: "",
PhotoName: "", PhotoName: "",
FileName: "",
PhotoTitle: "", PhotoTitle: "",
TitleSrc: "", TitleSrc: "",
PhotoDescription: "", PhotoDescription: "",

View file

@ -18,12 +18,14 @@
<p-photo-mosaic v-if="settings.view === 'mosaic'" <p-photo-mosaic v-if="settings.view === 'mosaic'"
:photos="results" :photos="results"
:selection="selection" :selection="selection"
:filter="filter"
:album="model" :album="model"
:edit-photo="editPhoto" :edit-photo="editPhoto"
:open-photo="openPhoto"></p-photo-mosaic> :open-photo="openPhoto"></p-photo-mosaic>
<p-photo-list v-else-if="settings.view === 'list'" <p-photo-list v-else-if="settings.view === 'list'"
:photos="results" :photos="results"
:selection="selection" :selection="selection"
:filter="filter"
:album="model" :album="model"
:open-photo="openPhoto" :open-photo="openPhoto"
:edit-photo="editPhoto" :edit-photo="editPhoto"
@ -31,6 +33,7 @@
<p-photo-cards v-else <p-photo-cards v-else
:photos="results" :photos="results"
:selection="selection" :selection="selection"
:filter="filter"
:album="model" :album="model"
:open-photo="openPhoto" :open-photo="openPhoto"
:edit-photo="editPhoto" :edit-photo="editPhoto"

View file

@ -16,17 +16,20 @@
<p-photo-mosaic v-if="settings.view === 'mosaic'" <p-photo-mosaic v-if="settings.view === 'mosaic'"
:photos="results" :photos="results"
:selection="selection" :selection="selection"
:filter="filter"
:edit-photo="editPhoto" :edit-photo="editPhoto"
:open-photo="openPhoto"></p-photo-mosaic> :open-photo="openPhoto"></p-photo-mosaic>
<p-photo-list v-else-if="settings.view === 'list'" <p-photo-list v-else-if="settings.view === 'list'"
:photos="results" :photos="results"
:selection="selection" :selection="selection"
:filter="filter"
:open-photo="openPhoto" :open-photo="openPhoto"
:edit-photo="editPhoto" :edit-photo="editPhoto"
:open-location="openLocation"></p-photo-list> :open-location="openLocation"></p-photo-list>
<p-photo-cards v-else <p-photo-cards v-else
:photos="results" :photos="results"
:selection="selection" :selection="selection"
:filter="filter"
:open-photo="openPhoto" :open-photo="openPhoto"
:edit-photo="editPhoto" :edit-photo="editPhoto"
:open-location="openLocation"></p-photo-cards> :open-location="openLocation"></p-photo-cards>

View file

@ -18,6 +18,7 @@ const (
FolderRootUnknown = "" FolderRootUnknown = ""
FolderRootOriginals = "originals" FolderRootOriginals = "originals"
FolderRootImport = "import" FolderRootImport = "import"
RootPath = "/"
) )
// Folder represents a file system directory. // Folder represents a file system directory.
@ -55,8 +56,8 @@ func NewFolder(root, pathName string, modTime *time.Time) Folder {
pathName = strings.Trim(pathName, string(os.PathSeparator)) pathName = strings.Trim(pathName, string(os.PathSeparator))
if pathName == "" { if pathName == RootPath {
pathName = "/" pathName = ""
} }
result := Folder{ result := Folder{
@ -79,7 +80,7 @@ func (m *Folder) SetTitleFromPath() {
s := m.Path s := m.Path
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
if s == "" || s == "/" { if s == "" || s == RootPath {
s = m.Root s = m.Root
} else { } else {
s = path.Base(s) s = path.Base(s)
@ -103,13 +104,23 @@ func (m *Folder) SetTitleFromPath() {
m.FolderTitle = txt.Clip(s, txt.ClipDefault) m.FolderTitle = txt.Clip(s, txt.ClipDefault)
} }
// Saves the entity using form data and stores it in the database. // Saves the complete entity in the database.
func (m *Folder) Save(f form.Folder) error { func (m *Folder) Create() error {
return Db().Create(m).Error
}
// Updates selected properties in the database.
func (m *Folder) Updates(values interface{}) error {
return Db().Model(m).Updates(values).Error
}
// SetForm updates the entity properties based on form values.
func (m *Folder) SetForm(f form.Folder) error {
if err := deepcopier.Copy(m).From(f); err != nil { if err := deepcopier.Copy(m).From(f); err != nil {
return err return err
} }
return Db().Save(m).Error return nil
} }
// Returns a slice of folders in a given directory incl sub directories in recursive mode. // Returns a slice of folders in a given directory incl sub directories in recursive mode.

View file

@ -40,9 +40,15 @@ func TestNewFolder(t *testing.T) {
assert.Equal(t, "23 Birthday", folder.FolderTitle) assert.Equal(t, "23 Birthday", folder.FolderTitle)
}) })
t.Run("name empty", func(t *testing.T) { t.Run("empty", func(t *testing.T) {
folder := NewFolder(FolderRootOriginals, "", nil) folder := NewFolder(FolderRootOriginals, "", nil)
assert.Equal(t, "/", folder.Path) assert.Equal(t, "", folder.Path)
assert.Equal(t, "Originals", folder.FolderTitle)
})
t.Run("root", func(t *testing.T) {
folder := NewFolder(FolderRootOriginals, RootPath, nil)
assert.Equal(t, "", folder.Path)
assert.Equal(t, "Originals", folder.FolderTitle) assert.Equal(t, "Originals", folder.FolderTitle)
}) })
} }

View file

@ -116,6 +116,14 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
} }
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip { if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
if isDir && result != filepath.SkipDir {
folder := entity.NewFolder(entity.FolderRootImport, fs.RelativeName(fileName, imp.conf.ImportPath()), nil)
if err := folder.Create(); err == nil {
log.Infof("import: added folder /%s", folder.Path)
}
}
return result return result
} }

View file

@ -120,6 +120,14 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
isSymlink := info.IsSymlink() isSymlink := info.IsSymlink()
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip { if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
if isDir && result != filepath.SkipDir {
folder := entity.NewFolder(entity.FolderRootOriginals, fs.RelativeName(fileName, originalsPath), nil)
if err := folder.Create(); err == nil {
log.Infof("index: added folder /%s", folder.Path)
}
}
return result return result
} }

View file

@ -309,17 +309,15 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
photo.SetTakenAt(takenUtc, takenUtc, "", takenSrc) photo.SetTakenAt(takenUtc, takenUtc, "", takenSrc)
} }
if photo.NoTitle() { if photo.HasLatLng() {
if photo.HasLatLng() { var locLabels classify.Labels
var locLabels classify.Labels locKeywords, locLabels = photo.UpdateLocation(ind.conf.GeoCodingApi())
locKeywords, locLabels = photo.UpdateLocation(ind.conf.GeoCodingApi()) labels = append(labels, locLabels...)
labels = append(labels, locLabels...) } else {
} else { log.Debugf("index: no coordinates in metadata for %s", txt.Quote(m.RelativeName(ind.originalsPath())))
log.Debugf("index: no coordinates in metadata for %s", txt.Quote(m.RelativeName(ind.originalsPath())))
photo.Place = &entity.UnknownPlace photo.Place = &entity.UnknownPlace
photo.PlaceID = entity.UnknownPlace.ID photo.PlaceID = entity.UnknownPlace.ID
}
} }
} }

View file

@ -289,6 +289,8 @@ func Photos(f form.PhotoSearch) (results PhotosResults, count int, err error) {
case entity.SortOrderSimilar: case entity.SortOrderSimilar:
s = s.Where("files.file_diff > 0") s = s.Where("files.file_diff > 0")
s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC, files.file_primary DESC") s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC, files.file_primary DESC")
case entity.SortOrderName:
s = s.Order("photos.photo_path, photos.photo_name, files.file_primary DESC")
default: default:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC") s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
} }

View file

@ -7,6 +7,10 @@ import (
// RelativeName returns the file name relative to directory. // RelativeName returns the file name relative to directory.
func RelativeName(fileName, directory string) string { func RelativeName(fileName, directory string) string {
if fileName == directory {
return ""
}
if index := strings.Index(fileName, directory); index == 0 { if index := strings.Index(fileName, directory); index == 0 {
if index := strings.LastIndex(directory, string(os.PathSeparator)); index == len(directory)-1 { if index := strings.LastIndex(directory, string(os.PathSeparator)); index == len(directory)-1 {
pos := len(directory) pos := len(directory)

View file

@ -7,6 +7,15 @@ import (
) )
func TestRelativeName(t *testing.T) { func TestRelativeName(t *testing.T) {
t.Run("same", func(t *testing.T) {
assert.Equal(t, "", RelativeName("/some/path", "/some/path"))
})
t.Run("short", func(t *testing.T) {
assert.Equal(t, "/some/", RelativeName("/some/", "/some/path"))
})
t.Run("empty", func(t *testing.T) {
assert.Equal(t, "", RelativeName("", "/some/path"))
})
t.Run("/some/path", func(t *testing.T) { t.Run("/some/path", func(t *testing.T) {
assert.Equal(t, "foo/bar.baz", RelativeName("/some/path/foo/bar.baz", "/some/path")) assert.Equal(t, "foo/bar.baz", RelativeName("/some/path/foo/bar.baz", "/some/path"))
}) })