Labels: Edit name in overview #212

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-02-02 02:00:47 +01:00
parent 90dd094a21
commit 1cbb0a6d56
15 changed files with 210 additions and 76 deletions

View File

@@ -61,13 +61,12 @@
<v-card tile class="accent lighten-3" <v-card tile class="accent lighten-3"
slot-scope="{ hover }" slot-scope="{ hover }"
:class="selection.includes(album.AlbumUUID) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'" :class="selection.includes(album.AlbumUUID) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'"
:to="{name: 'album', params: {uuid: album.AlbumUUID, slug: album.AlbumSlug}}"
> >
<v-img <v-img
:src="album.getThumbnailUrl('tile_500')" :src="album.getThumbnailUrl('tile_500')"
aspect-ratio="1" aspect-ratio="1"
style="cursor: pointer"
class="accent lighten-2" class="accent lighten-2"
@click.prevent="openAlbum(index)"
> >
<v-layout <v-layout
slot="placeholder" slot="placeholder"
@@ -90,7 +89,7 @@
</v-btn> </v-btn>
</v-img> </v-img>
<v-card-actions> <v-card-actions @click.stop.prevent="">
<v-edit-dialog <v-edit-dialog
:return-value.sync="album.AlbumName" :return-value.sync="album.AlbumName"
lazy lazy
@@ -183,10 +182,6 @@
this.filter.q = ''; this.filter.q = '';
this.search(); this.search();
}, },
openAlbum(index) {
const album = this.results[index];
this.$router.push({name: "album", params: {uuid: album.AlbumUUID, slug: album.AlbumSlug}});
},
loadMore() { loadMore() {
if (this.scrollDisabled) return; if (this.scrollDisabled) return;

View File

@@ -33,7 +33,7 @@
</v-form> </v-form>
<v-container fluid class="pa-4" v-if="loading"> <v-container fluid class="pa-4" v-if="loading">
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear> <v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
</v-container> </v-container>
<v-container fluid class="pa-0" v-else> <v-container fluid class="pa-0" v-else>
<p-scroll-top></p-scroll-top> <p-scroll-top></p-scroll-top>
@@ -42,8 +42,12 @@
<v-card v-if="results.length === 0" class="p-labels-empty secondary-light lighten-1" flat> <v-card v-if="results.length === 0" class="p-labels-empty secondary-light lighten-1" flat>
<v-card-title primary-title> <v-card-title primary-title>
<div> <div>
<h3 class="title mb-3"><translate>No labels matched your search</translate></h3> <h3 class="title mb-3">
<div><translate>Try again using a related or otherwise similar term.</translate></div> <translate>No labels matched your search</translate>
</h3>
<div>
<translate>Try again using a related or otherwise similar term.</translate>
</div>
</div> </div>
</v-card-title> </v-card-title>
</v-card> </v-card>
@@ -55,13 +59,12 @@
xs6 sm4 md3 lg2 d-flex xs6 sm4 md3 lg2 d-flex
> >
<v-hover> <v-hover>
<v-card tile class="elevation-0 ma-1 accent lighten-3"> <v-card tile class="elevation-0 ma-1 accent lighten-3"
:to="{name: 'photos', query: {q: 'label:' + label.LabelSlug}}">
<v-img <v-img
:src="label.getThumbnailUrl('tile_500')" :src="label.getThumbnailUrl('tile_500')"
aspect-ratio="1" aspect-ratio="1"
style="cursor: pointer"
class="accent lighten-2" class="accent lighten-2"
@click.prevent="openLabel(index)"
> >
<v-layout <v-layout
slot="placeholder" slot="placeholder"
@@ -70,12 +73,35 @@
justify-center justify-center
ma-0 ma-0
> >
<v-progress-circular indeterminate color="accent lighten-5"></v-progress-circular> <v-progress-circular indeterminate
color="accent lighten-5"></v-progress-circular>
</v-layout> </v-layout>
</v-img> </v-img>
<v-card-actions> <v-card-actions @click.stop.prevent="">
{{ label.LabelName | capitalize }} <v-edit-dialog
:return-value.sync="label.LabelName"
lazy
@save="onSave(label)"
class="p-inline-edit"
>
<span v-if="label.LabelName">
{{ label.LabelName | capitalize }}
</span>
<span v-else>
<v-icon>edit</v-icon>
</span>
<template v-slot:input>
<v-text-field
v-model="label.LabelName"
:rules="[titleRule]"
:label="labels.name"
color="secondary-dark"
single-line
autofocus
></v-text-field>
</template>
</v-edit-dialog>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click.stop.prevent="label.toggleLike()"> <v-btn icon @click.stop.prevent="label.toggleLike()">
<v-icon v-if="label.LabelFavorite" color="#FFD600">star <v-icon v-if="label.LabelFavorite" color="#FFD600">star
@@ -132,10 +158,15 @@
routeName: routeName, routeName: routeName,
labels: { labels: {
search: this.$gettext("Search"), search: this.$gettext("Search"),
name: this.$gettext("Label Name"),
}, },
titleRule: v => v.length <= 25 || this.$gettext("Name too long"),
}; };
}, },
methods: { methods: {
onSave(label) {
label.update();
},
showAll() { showAll() {
this.filter.all = "true"; this.filter.all = "true";
this.updateQuery(); this.updateQuery();
@@ -148,10 +179,6 @@
this.filter.q = ''; this.filter.q = '';
this.updateQuery(); this.updateQuery();
}, },
openLabel(index) {
const label = this.results[index];
this.$router.push({name: "photos", query: {q: "label:" + label.LabelSlug}});
},
loadMore() { loadMore() {
if (this.scrollDisabled) return; if (this.scrollDisabled) return;

View File

@@ -144,7 +144,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Save(&m) conf.Db().Save(&m)
event.Publish("config.updated", event.Data(conf.ClientConfig())) event.Publish("config.updated", event.Data(conf.ClientConfig()))
event.Success(fmt.Sprintf("album \"%s\" saved", m.AlbumName)) event.Success("album saved")
PublishAlbumEvent(EntityUpdated, id, c, q) PublishAlbumEvent(EntityUpdated, id, c, q)

View File

@@ -14,5 +14,6 @@ var (
ErrUploadNSFW = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrUploadNSFW.Error())} ErrUploadNSFW = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrUploadNSFW.Error())}
ErrAlbumNotFound = gin.H{"code": http.StatusNotFound, "error": "Album not found"} ErrAlbumNotFound = gin.H{"code": http.StatusNotFound, "error": "Album not found"}
ErrPhotoNotFound = gin.H{"code": http.StatusNotFound, "error": "Photo not found"} ErrPhotoNotFound = gin.H{"code": http.StatusNotFound, "error": "Photo not found"}
ErrLabelNotFound = gin.H{"code": http.StatusNotFound, "error": "Label not found"}
ErrUnexpectedError = gin.H{"code": http.StatusInternalServerError, "error": "Unexpected error"} ErrUnexpectedError = gin.H{"code": http.StatusInternalServerError, "error": "Unexpected error"}
) )

View File

@@ -50,6 +50,42 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
}) })
} }
// PUT /api/v1/labels/:uuid
func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
router.PUT("/labels/:uuid", func(c *gin.Context) {
if Unauthorized(c, conf) {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
return
}
var f form.Label
if err := c.BindJSON(&f); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst(err.Error())})
return
}
id := c.Param("uuid")
q := query.New(conf.OriginalsPath(), conf.Db())
m, err := q.FindLabelByUUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
return
}
m.Rename(f.LabelName)
conf.Db().Save(&m)
event.Success("label saved")
PublishLabelEvent(EntityUpdated, id, c, q)
c.JSON(http.StatusOK, m)
})
}
// POST /api/v1/labels/:uuid/like // POST /api/v1/labels/:uuid/like
// //
// Parameters: // Parameters:

View File

@@ -64,7 +64,7 @@ var rules = LabelRules{
}, },
"african chameleon": { "african chameleon": {
Label: "chameleon", Label: "chameleon",
Threshold: 0.500000, Threshold: 0.900000,
Priority: 1, Priority: 1,
Categories: []string{"reptile", "animal", "lizard"}, Categories: []string{"reptile", "animal", "lizard"},
}, },
@@ -148,9 +148,9 @@ var rules = LabelRules{
}, },
"altar": { "altar": {
Label: "", Label: "",
Threshold: 0.300000, Threshold: 0.500000,
Priority: 0, Priority: 0,
Categories: []string{}, Categories: []string{"church"},
}, },
"ambulance": { "ambulance": {
Label: "van", Label: "van",
@@ -172,7 +172,7 @@ var rules = LabelRules{
}, },
"american chameleon": { "american chameleon": {
Label: "chameleon", Label: "chameleon",
Threshold: 0.500000, Threshold: 0.900000,
Priority: 1, Priority: 1,
Categories: []string{"reptile", "animal", "lizard"}, Categories: []string{"reptile", "animal", "lizard"},
}, },
@@ -520,7 +520,7 @@ var rules = LabelRules{
}, },
"beacon": { "beacon": {
Label: "", Label: "",
Threshold: 0.000000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"tower", "architecture"}, Categories: []string{"tower", "architecture"},
}, },
@@ -664,8 +664,8 @@ var rules = LabelRules{
}, },
"binoculars": { "binoculars": {
Label: "", Label: "",
Threshold: 0.500000, Threshold: 1.000000,
Priority: 0, Priority: -2,
Categories: []string{}, Categories: []string{},
}, },
"bird": { "bird": {
@@ -729,10 +729,10 @@ var rules = LabelRules{
Categories: []string{"animal"}, Categories: []string{"animal"},
}, },
"black-footed ferret": { "black-footed ferret": {
Label: "", Label: "animal",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"animal"}, Categories: []string{},
}, },
"blenheim spaniel dog": { "blenheim spaniel dog": {
Label: "dog", Label: "dog",
@@ -855,9 +855,9 @@ var rules = LabelRules{
Categories: []string{"animal"}, Categories: []string{"animal"},
}, },
"bow": { "bow": {
Label: "portrait", Label: "",
Threshold: 0.200000, Threshold: 1.000000,
Priority: 0, Priority: -2,
Categories: []string{}, Categories: []string{},
}, },
"bow tie": { "bow tie": {
@@ -1240,7 +1240,7 @@ var rules = LabelRules{
}, },
"chameleon": { "chameleon": {
Label: "chameleon", Label: "chameleon",
Threshold: 0.500000, Threshold: 0.900000,
Priority: 1, Priority: 1,
Categories: []string{"reptile", "animal", "lizard"}, Categories: []string{"reptile", "animal", "lizard"},
}, },
@@ -1731,10 +1731,10 @@ var rules = LabelRules{
Categories: []string{"reptile", "animal"}, Categories: []string{"reptile", "animal"},
}, },
"diaper": { "diaper": {
Label: "baby", Label: "",
Threshold: 0.250000, Threshold: 1.000000,
Priority: 0, Priority: -2,
Categories: []string{"people"}, Categories: []string{},
}, },
"digital clock": { "digital clock": {
Label: "display", Label: "display",
@@ -3621,10 +3621,10 @@ var rules = LabelRules{
Categories: []string{"vehicle"}, Categories: []string{"vehicle"},
}, },
"mink": { "mink": {
Label: "", Label: "animal",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"animal"}, Categories: []string{},
}, },
"missile": { "missile": {
Label: "", Label: "",
@@ -4293,10 +4293,10 @@ var rules = LabelRules{
Categories: []string{}, Categories: []string{},
}, },
"polecat": { "polecat": {
Label: "", Label: "animal",
Threshold: 0.400000, Threshold: 0.400000,
Priority: 0, Priority: 0,
Categories: []string{"animal"}, Categories: []string{},
}, },
"police van": { "police van": {
Label: "van", Label: "van",

View File

@@ -417,7 +417,7 @@ common iguana:
chameleon: chameleon:
label: chameleon label: chameleon
threshold: 0.5 threshold: 0.9
priority: 1 priority: 1
categories: categories:
- reptile - reptile
@@ -1578,19 +1578,16 @@ weasel:
- animal - animal
mink: mink:
label: animal
threshold: 0.4 threshold: 0.4
categories:
- animal
polecat: polecat:
label: animal
threshold: 0.4 threshold: 0.4
categories:
- animal
black-footed ferret: black-footed ferret:
label: animal
threshold: 0.4 threshold: 0.4
categories:
- animal
otter: otter:
threshold: 0.4 threshold: 0.4
@@ -1873,6 +1870,7 @@ bathtub:
see: living see: living
beacon: beacon:
threshold: 0.4
categories: categories:
- tower - tower
- architecture - architecture
@@ -3603,7 +3601,7 @@ bassinet:
see: baby see: baby
diaper: diaper:
see: baby see: ignore
patio: patio:
label: building label: building
@@ -3693,7 +3691,9 @@ sundial:
threshold: 0.5 threshold: 0.5
altar: altar:
threshold: 0.3 threshold: 0.5
categories:
- church
ignore: ignore:
threshold: 1 threshold: 1
@@ -3777,11 +3777,10 @@ barometer:
see: display see: display
binoculars: binoculars:
threshold: 0.5 see: ignore
bow: bow:
label: portrait see: ignore
threshold: 0.2
caldron: caldron:
label: cooking label: cooking

View File

@@ -8,6 +8,7 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
) )
// Labels for photo, album and location categorization // Labels for photo, album and location categorization
@@ -68,3 +69,13 @@ func (m *Label) FirstOrCreate(db *gorm.DB) *Label {
func (m *Label) AfterCreate(scope *gorm.Scope) error { func (m *Label) AfterCreate(scope *gorm.Scope) error {
return scope.SetColumn("New", true) return scope.SetColumn("New", true)
} }
func (m *Label) Rename(name string) {
name = txt.Clip(name, 128)
if name == "" {
name = txt.SlugToTitle(m.LabelSlug)
}
m.LabelName = name
}

View File

@@ -39,6 +39,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.GetMomentsTime(v1, conf) api.GetMomentsTime(v1, conf)
api.GetLabels(v1, conf) api.GetLabels(v1, conf)
api.UpdateLabel(v1, conf)
api.LikeLabel(v1, conf) api.LikeLabel(v1, conf)
api.DislikeLabel(v1, conf) api.DislikeLabel(v1, conf)
api.LabelThumbnail(v1, conf) api.LabelThumbnail(v1, conf)

18
pkg/txt/clip.go Normal file
View File

@@ -0,0 +1,18 @@
package txt
import "strings"
func Clip(s string, size int) string {
if s == "" {
return ""
}
s = strings.TrimSpace(s)
runes := []rune(s)
if len(runes) > size {
s = string(runes[0:size-1])
}
return s
}

19
pkg/txt/clip_test.go Normal file
View File

@@ -0,0 +1,19 @@
package txt
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestClip(t *testing.T) {
t.Run("clip", func(t *testing.T) {
assert.Equal(t, "I'm ä", Clip("I'm ä lazy BRoWN fox!", 6))
})
t.Run("ok", func(t *testing.T) {
assert.Equal(t, "I'm ä lazy BRoWN fox!", Clip("I'm ä lazy BRoWN fox!", 128))
})
t.Run("empty", func(t *testing.T) {
assert.Equal(t, "", Clip("", -1))
})
}

View File

@@ -1,18 +0,0 @@
package txt
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestKeywords(t *testing.T) {
t.Run("cat", func(t *testing.T) {
keywords := Keywords("cat")
assert.Equal(t, []string{"cat"}, keywords)
})
t.Run("was", func(t *testing.T) {
keywords := Keywords("was")
assert.Equal(t, []string(nil), keywords)
})
}

13
pkg/txt/slug.go Normal file
View File

@@ -0,0 +1,13 @@
package txt
import "strings"
// SlugToTitle converts a slug back to a title
func SlugToTitle(s string) string {
if s == "" {
return ""
}
return Title(strings.Join(Words(s), " "))
}

View File

@@ -7,11 +7,14 @@ import (
var KeywordsRegexp = regexp.MustCompile("[\\p{L}]{3,}") var KeywordsRegexp = regexp.MustCompile("[\\p{L}]{3,}")
// Keywords extracts keywords for indexing and returns them as string slice. // Words returns a slice of words with at least 3 characters from a string.
func Keywords(s string) (results []string) { func Words(s string) (results []string) {
all := KeywordsRegexp.FindAllString(s, -1) return KeywordsRegexp.FindAllString(s, -1)
}
for _, w := range all { // Keywords returns a slice of keywords without stopwords.
func Keywords(s string) (results []string) {
for _, w := range Words(s) {
w = strings.ToLower(w) w = strings.ToLower(w)
if _, ok := Stopwords[w]; ok == false { if _, ok := Stopwords[w]; ok == false {

29
pkg/txt/words_test.go Normal file
View File

@@ -0,0 +1,29 @@
package txt
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWords(t *testing.T) {
t.Run("I'm a lazy brown fox!", func(t *testing.T) {
result := Words("I'm a lazy BRoWN fox!")
assert.Equal(t, []string{"lazy", "BRoWN", "fox"}, result)
})
t.Run("no result", func(t *testing.T) {
result := Words("x")
assert.Equal(t, []string(nil), result)
})
}
func TestKeywords(t *testing.T) {
t.Run("I'm a lazy brown fox!", func(t *testing.T) {
result := Keywords("I'm a lazy BRoWN img!")
assert.Equal(t, []string{"lazy", "brown"}, result)
})
t.Run("no result", func(t *testing.T) {
result := Keywords("was")
assert.Equal(t, []string(nil), result)
})
}