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

View file

@ -33,7 +33,7 @@
</v-form>
<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 fluid class="pa-0" v-else>
<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-title primary-title>
<div>
<h3 class="title mb-3"><translate>No labels matched your search</translate></h3>
<div><translate>Try again using a related or otherwise similar term.</translate></div>
<h3 class="title mb-3">
<translate>No labels matched your search</translate>
</h3>
<div>
<translate>Try again using a related or otherwise similar term.</translate>
</div>
</div>
</v-card-title>
</v-card>
@ -55,13 +59,12 @@
xs6 sm4 md3 lg2 d-flex
>
<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
:src="label.getThumbnailUrl('tile_500')"
aspect-ratio="1"
style="cursor: pointer"
class="accent lighten-2"
@click.prevent="openLabel(index)"
>
<v-layout
slot="placeholder"
@ -70,12 +73,35 @@
justify-center
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-img>
<v-card-actions>
{{ label.LabelName | capitalize }}
<v-card-actions @click.stop.prevent="">
<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-btn icon @click.stop.prevent="label.toggleLike()">
<v-icon v-if="label.LabelFavorite" color="#FFD600">star
@ -132,10 +158,15 @@
routeName: routeName,
labels: {
search: this.$gettext("Search"),
name: this.$gettext("Label Name"),
},
titleRule: v => v.length <= 25 || this.$gettext("Name too long"),
};
},
methods: {
onSave(label) {
label.update();
},
showAll() {
this.filter.all = "true";
this.updateQuery();
@ -148,10 +179,6 @@
this.filter.q = '';
this.updateQuery();
},
openLabel(index) {
const label = this.results[index];
this.$router.push({name: "photos", query: {q: "label:" + label.LabelSlug}});
},
loadMore() {
if (this.scrollDisabled) return;

View file

@ -144,7 +144,7 @@ func UpdateAlbum(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Save(&m)
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)

View file

@ -14,5 +14,6 @@ var (
ErrUploadNSFW = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrUploadNSFW.Error())}
ErrAlbumNotFound = gin.H{"code": http.StatusNotFound, "error": "Album 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"}
)

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
//
// Parameters:

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
)
// 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 {
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.GetLabels(v1, conf)
api.UpdateLabel(v1, conf)
api.LikeLabel(v1, conf)
api.DislikeLabel(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,}")
// Keywords extracts keywords for indexing and returns them as string slice.
func Keywords(s string) (results []string) {
all := KeywordsRegexp.FindAllString(s, -1)
// Words returns a slice of words with at least 3 characters from a string.
func Words(s string) (results []string) {
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)
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)
})
}