Fix indexer and add sort by file name #328
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
a7122ff4e1
commit
e796d036c2
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')},
|
||||||
],
|
],
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -122,6 +122,7 @@
|
||||||
openPhoto: Function,
|
openPhoto: Function,
|
||||||
editPhoto: Function,
|
editPhoto: Function,
|
||||||
album: Object,
|
album: Object,
|
||||||
|
filter: Object,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -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')},
|
||||||
],
|
],
|
||||||
|
|
|
@ -24,6 +24,7 @@ export class Photo extends RestModel {
|
||||||
TimeZone: "",
|
TimeZone: "",
|
||||||
PhotoPath: "",
|
PhotoPath: "",
|
||||||
PhotoName: "",
|
PhotoName: "",
|
||||||
|
FileName: "",
|
||||||
PhotoTitle: "",
|
PhotoTitle: "",
|
||||||
TitleSrc: "",
|
TitleSrc: "",
|
||||||
PhotoDescription: "",
|
PhotoDescription: "",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"))
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue