Labels: Edit name in overview #212
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
90dd094a21
commit
1cbb0a6d56
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"}
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
18
pkg/txt/clip.go
Normal 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
19
pkg/txt/clip_test.go
Normal 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))
|
||||
})
|
||||
}
|
|
@ -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
13
pkg/txt/slug.go
Normal 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), " "))
|
||||
}
|
||||
|
|
@ -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
29
pkg/txt/words_test.go
Normal 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)
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue