Refactor string clipping in frontend & backend

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-04-26 14:31:33 +02:00
parent b9a82d098e
commit 882340a14c
47 changed files with 189 additions and 147 deletions

View file

@ -25,7 +25,7 @@ import VueInfiniteScroll from "vue-infinite-scroll";
// Initialize helpers
const viewer = new Viewer();
const clipboard = new Clipboard(window.localStorage, "photo_clipboard");
const isPublic = config.getValue("public");
const isPublic = config.get("public");
// Assign helpers to VueJS prototype
Vue.prototype.$event = Event;

View file

@ -23,8 +23,8 @@ class Config {
Event.subscribe("config.updated", (ev, data) => this.setValues(data));
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
if (this.hasValue("settings")) {
this.setTheme(this.getValue("settings").theme);
if (this.has("settings")) {
this.setTheme(this.get("settings").theme);
} else {
this.setTheme("default");
}
@ -39,7 +39,7 @@ class Config {
for (let key in values) {
if (values.hasOwnProperty(key)) {
this.setValue(key, values[key]);
this.set(key, values[key]);
}
}
@ -99,30 +99,22 @@ class Config {
storeValues() {
this.storage.setItem(this.storage_key, JSON.stringify(this.getValues()));
return this;
}
setValue(key, value) {
set(key, value) {
this.values[key] = value;
return this;
}
hasValue(key) {
has(key) {
return !!this.values[key];
}
getValue(key) {
get(key) {
return this.values[key];
}
deleteValue(key) {
delete this.values[key];
return this;
}
feature(name) {
return this.values.settings.features[name];
}

View file

@ -134,11 +134,11 @@
const cameras = [{
ID: 0,
CameraModel: this.$gettext('All Cameras')
}].concat(this.$config.getValue('cameras'));
}].concat(this.$config.get('cameras'));
const countries = [{
code: '',
name: this.$gettext('All Countries')
}].concat(this.$config.getValue('countries'));
}].concat(this.$config.get('countries'));
return {
searchExpanded: false,
@ -167,7 +167,7 @@
sort: this.$gettext("Sort By"),
name: this.$gettext("Album Name"),
},
titleRule: v => v.length <= 25 || this.$gettext("Title too long"),
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"),
growDesc: false,
};
},

View file

@ -304,8 +304,8 @@
drawer: null,
mini: true,
session: this.$session,
public: this.$config.getValue("public"),
readonly: this.$config.getValue("readonly"),
public: this.$config.get("public"),
readonly: this.$config.get("readonly"),
config: this.$config.values,
page: this.$config.page,
upload: {

View file

@ -329,7 +329,7 @@
{"value": 86400 * 365, "text": "After one year"},
],
},
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
options: options,
label: {
cancel: this.$gettext("Cancel"),

View file

@ -102,7 +102,7 @@
loading: false,
search: null,
items: [],
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
active: this.tab,
}
},

View file

@ -69,7 +69,7 @@
total: 0,
completed: 0,
started: 0,
safe: !this.$config.getValue("uploadNSFW")
safe: !this.$config.get("uploadNSFW")
}
},
methods: {

View file

@ -38,6 +38,7 @@
<v-flex xs12 class="pa-2">
<v-text-field
:disabled="disabled"
:rules="[textRule]"
hide-details
label="Title"
placeholder=""
@ -257,6 +258,7 @@
<v-flex xs12 sm6 md3 class="pa-2">
<v-textarea
:disabled="disabled"
:rules="[textRule]"
hide-details
browser-autocomplete="off"
auto-grow
@ -271,6 +273,7 @@
<v-flex xs12 sm6 md3 class="pa-2">
<v-text-field
:disabled="disabled"
:rules="[textRule]"
hide-details
browser-autocomplete="off"
label="Artist"
@ -283,6 +286,7 @@
<v-flex xs12 sm6 md3 class="pa-2">
<v-text-field
:disabled="disabled"
:rules="[textRule]"
hide-details
browser-autocomplete="off"
label="Copyright"
@ -295,6 +299,7 @@
<v-flex xs12 sm6 md3 class="pa-2">
<v-textarea
:disabled="disabled"
:rules="[textRule]"
hide-details
browser-autocomplete="off"
auto-grow
@ -393,7 +398,7 @@
all: {
colors: [{label: "Unknown", name: ""}],
},
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
options: options,
countries: countries,
labels: {
@ -418,6 +423,7 @@
dateFormatted: "",
timeFormatted: "",
timeLocalFormatted: "",
textRule: v => v.length <= this.$config.get('clip') || this.$gettext("Text too long"),
};
},
watch: {

View file

@ -47,7 +47,7 @@
data() {
return {
config: this.$config.values,
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
selected: [],
listColumns: [
{text: this.$gettext('Primary'), value: 'FilePrimary', sortable: false, align: 'center', class: 'p-col-primary'},

View file

@ -97,7 +97,7 @@
return {
disabled: !this.$config.feature("edit"),
config: this.$config.values,
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
selected: [],
newLabel: "",
listColumns: [
@ -111,7 +111,7 @@
search: this.$gettext("Search"),
name: this.$gettext("Label Name"),
},
nameRule: v => v.length <= 25 || this.$gettext("Name too long"),
nameRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
};
},
computed: {},

View file

@ -178,7 +178,7 @@
filter: filter,
lastFilter: {},
routeName: routeName,
titleRule: v => v.length <= 25 || this.$gettext("Title too long"),
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Title too long"),
labels: {
search: this.$gettext("Search"),
name: this.$gettext("Album Name"),

View file

@ -60,7 +60,7 @@
},
data() {
return {
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
active: this.tab,
}
},

View file

@ -33,7 +33,7 @@
name: 'p-tab-discover-colors',
data() {
return {
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
colors: this.$config.values.colors,
labels: {},
};

View file

@ -15,7 +15,7 @@
name: 'p-tab-discover-todo',
data() {
return {
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
config: this.$config.values,
labels: {},
};

View file

@ -184,7 +184,7 @@
search: this.$gettext("Search"),
name: this.$gettext("Label Name"),
},
titleRule: v => v.length <= 25 || this.$gettext("Name too long"),
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
mouseDown: {
index: -1,
timeStamp: -1,

View file

@ -54,7 +54,7 @@
},
data() {
return {
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
active: this.tab,
}
},

View file

@ -98,7 +98,7 @@
data() {
return {
settings: new Settings(this.$config.settings()),
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
started: false,
busy: false,
completed: 0,

View file

@ -55,7 +55,7 @@
total: 0,
completed: 0,
started: 0,
safe: !this.$config.getValue("uploadNSFW")
safe: !this.$config.get("uploadNSFW")
}
},
methods: {

View file

@ -44,7 +44,7 @@
},
data() {
return {
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
active: this.tab,
}
},

View file

@ -78,7 +78,7 @@
data() {
return {
config: this.$config.values,
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
settings: new Settings(this.$config.values.settings),
options: options,
model: {},

View file

@ -280,8 +280,8 @@
<v-card-actions>
<v-layout wrap align-top>
<v-flex xs12 sm6 class="px-2 pb-2 body-1">
PhotoPrism {{$config.getValue("version")}}
<br>{{$config.getValue("copyright")}}
PhotoPrism {{$config.get("version")}}
<br>{{$config.get("copyright")}}
</v-flex>
<v-flex xs12 sm6 class="px-2 pb-2 body-1 text-xs-left text-sm-right">
@ -307,7 +307,7 @@
name: 'p-settings-general',
data() {
return {
readonly: this.$config.getValue("readonly"),
readonly: this.$config.get("readonly"),
settings: new Settings(this.$config.settings()),
options: options,
labels: {

View file

@ -49,8 +49,8 @@ describe("common/config", () => {
const values = {};
const config = new Config(storage, values);
config.setValue("city", "Berlin");
const result = config.getValue("city");
config.set("city", "Berlin");
const result = config.get("city");
assert.equal(result, "Berlin");
});
});

View file

@ -76,7 +76,7 @@ func UpdateLabel(router *gin.RouterGroup, conf *config.Config) {
return
}
m.Rename(f.LabelName)
m.SetName(f.LabelName)
conf.Db().Save(&m)
event.Success("label saved")

View file

@ -32,5 +32,5 @@ func LocationLabel(name string, uncertainty int, priority int) Label {
// Title returns a formatted label title as string.
func (l Label) Title() string {
return txt.Title(txt.Clip(strings.TrimSpace(l.Name), 128))
return txt.Title(txt.Clip(l.Name, txt.ClipDefault))
}

View file

@ -7,6 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
// ClientConfig contains HTTP client / Web UI config values
@ -96,6 +97,7 @@ func (c *Config) PublicClientConfig() ClientConfig {
"years": []int{},
"colors": colors.All.List(),
"categories": []string{},
"clip": txt.ClipDefault,
}
return result
@ -241,6 +243,7 @@ func (c *Config) ClientConfig() ClientConfig {
"years": years,
"colors": colors.All.List(),
"categories": categories,
"clip": txt.ClipDefault,
}
return result

View file

@ -23,22 +23,22 @@ const (
// Account represents a remote service account for uploading, downloading or syncing media files.
type Account struct {
ID uint `gorm:"primary_key"`
AccName string `gorm:"type:varchar(128);"`
AccOwner string `gorm:"type:varchar(128);"`
AccName string `gorm:"type:varchar(255);"`
AccOwner string `gorm:"type:varchar(255);"`
AccURL string `gorm:"type:varbinary(512);"`
AccType string `gorm:"type:varbinary(256);"`
AccKey string `gorm:"type:varbinary(256);"`
AccUser string `gorm:"type:varbinary(256);"`
AccPass string `gorm:"type:varbinary(256);"`
AccType string `gorm:"type:varbinary(255);"`
AccKey string `gorm:"type:varbinary(255);"`
AccUser string `gorm:"type:varbinary(255);"`
AccPass string `gorm:"type:varbinary(255);"`
AccError string `gorm:"type:varbinary(512);"`
AccErrors int
AccShare bool
AccSync bool
RetryLimit int
SharePath string `gorm:"type:varbinary(256);"`
SharePath string `gorm:"type:varbinary(255);"`
ShareSize string `gorm:"type:varbinary(16);"`
ShareExpires int
SyncPath string `gorm:"type:varbinary(256);"`
SyncPath string `gorm:"type:varbinary(255);"`
SyncStatus string `gorm:"type:varbinary(16);"`
SyncInterval int
SyncDate sql.NullTime `deepcopier:"skip"`

View file

@ -8,6 +8,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
)
@ -16,12 +17,12 @@ type Album struct {
ID uint `gorm:"primary_key"`
CoverUUID string `gorm:"type:varbinary(36);"`
AlbumUUID string `gorm:"type:varbinary(36);unique_index;"`
AlbumSlug string `gorm:"type:varbinary(128);index;"`
AlbumName string `gorm:"type:varchar(128);"`
AlbumSlug string `gorm:"type:varbinary(255);index;"`
AlbumName string `gorm:"type:varchar(255);"`
AlbumDescription string `gorm:"type:text;"`
AlbumNotes string `gorm:"type:text;"`
AlbumOrder string `gorm:"type:varbinary(32);"`
AlbumTemplate string `gorm:"type:varbinary(256);"`
AlbumTemplate string `gorm:"type:varbinary(255);"`
AlbumFavorite bool
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:AlbumUUID"`
CreatedAt time.Time
@ -39,32 +40,36 @@ func (m *Album) BeforeCreate(scope *gorm.Scope) error {
}
// NewAlbum creates a new album; default name is current month and year
func NewAlbum(albumName string) *Album {
albumName = strings.TrimSpace(albumName)
if albumName == "" {
albumName = time.Now().Format("January 2006")
}
albumSlug := slug.Make(albumName)
func NewAlbum(name string) *Album {
now := time.Now().UTC()
result := &Album{
AlbumSlug: albumSlug,
AlbumName: albumName,
AlbumUUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,
CreatedAt: now,
UpdatedAt: now,
}
result.SetName(name)
return result
}
// Rename an existing album
func (m *Album) Rename(albumName string) {
if albumName == "" {
albumName = m.CreatedAt.Format("January 2006")
// SetName changes the album name.
func (m *Album) SetName(name string) {
name = strings.TrimSpace(name)
if name == "" {
name = m.CreatedAt.Format("January 2006")
}
m.AlbumName = strings.TrimSpace(albumName)
m.AlbumSlug = slug.Make(m.AlbumName)
m.AlbumName = txt.Clip(name, txt.ClipDefault)
if len(m.AlbumName) < txt.ClipSlug {
m.AlbumSlug = slug.Make(m.AlbumName)
} else {
m.AlbumSlug = slug.Make(txt.Clip(m.AlbumName, txt.ClipSlug)) + "-" + m.AlbumUUID
}
}
// Save updates the entity using form data and stores it in the database.
@ -74,7 +79,7 @@ func (m *Album) Save(f form.Album, db *gorm.DB) error {
}
if f.AlbumName != "" {
m.Rename(f.AlbumName)
m.SetName(f.AlbumName)
}
return db.Save(m).Error

View file

@ -5,6 +5,7 @@ import (
"time"
"github.com/gosimple/slug"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/stretchr/testify/assert"
)
@ -25,22 +26,38 @@ func TestNewAlbum(t *testing.T) {
})
}
func TestRename(t *testing.T) {
func TestAlbum_SetName(t *testing.T) {
t.Run("valid name", func(t *testing.T) {
album := NewAlbum("initial name")
assert.Equal(t, "initial name", album.AlbumName)
assert.Equal(t, "initial-name", album.AlbumSlug)
album.Rename("new album name")
assert.Equal(t, "new album name", album.AlbumName)
album.SetName("New Album Name")
assert.Equal(t, "New Album Name", album.AlbumName)
assert.Equal(t, "new-album-name", album.AlbumSlug)
})
t.Run("empty name", func(t *testing.T) {
album := NewAlbum("initial name")
assert.Equal(t, "initial name", album.AlbumName)
assert.Equal(t, "initial-name", album.AlbumSlug)
t.Log(album.CreatedAt)
album.Rename("")
assert.Equal(t, "January 0001", album.AlbumName)
assert.Equal(t, "january-0001", album.AlbumSlug)
album.SetName("")
expected := album.CreatedAt.Format("January 2006")
assert.Equal(t, expected, album.AlbumName)
assert.Equal(t, slug.Make(expected), album.AlbumSlug)
})
t.Run("long name", func(t *testing.T) {
longName := `A value in decimal degrees to a precision of 4 decimal places is precise to 11.132 meters at the
equator. A value in decimal degrees to 5 decimal places is precise to 1.1132 meter at the equator. Elevation also
introduces a small error. At 6,378 m elevation, the radius and surface distance is increased by 0.001 or 0.1%.
Because the earth is not flat, the precision of the longitude part of the coordinates increases
the further from the equator you get. The precision of the latitude part does not increase so much,
more strictly however, a meridian arc length per 1 second depends on the latitude at the point in question.
The discrepancy of 1 second meridian arc length between equator and pole is about 0.3 metres because the earth
is an oblate spheroid.`
expected := txt.Clip(longName, txt.ClipDefault)
slugExpected := txt.Clip(longName, txt.ClipSlug)
album := NewAlbum(longName)
assert.Equal(t, expected, album.AlbumName)
assert.Contains(t, album.AlbumSlug, slug.Make(slugExpected))
})
}

View file

@ -8,15 +8,16 @@ import (
"github.com/gosimple/slug"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/txt"
)
// Camera model and make (as extracted from UpdateExif metadata)
type Camera struct {
ID uint `gorm:"primary_key"`
CameraSlug string `gorm:"type:varbinary(128);unique_index;"`
CameraModel string `gorm:"type:varchar(128);"`
CameraMake string `gorm:"type:varchar(128);"`
CameraType string `gorm:"type:varchar(128);"`
CameraSlug string `gorm:"type:varbinary(255);unique_index;"`
CameraModel string `gorm:"type:varchar(255);"`
CameraMake string `gorm:"type:varchar(255);"`
CameraType string `gorm:"type:varchar(255);"`
CameraDescription string `gorm:"type:text;"`
CameraNotes string `gorm:"type:text;"`
CreatedAt time.Time
@ -37,7 +38,8 @@ func CreateUnknownCamera(db *gorm.DB) {
// NewCamera creates a camera entity from a model name and a make name.
func NewCamera(modelName string, makeName string) *Camera {
makeName = strings.TrimSpace(makeName)
modelName = txt.Clip(modelName, txt.ClipDefault)
makeName = txt.Clip(makeName, txt.ClipDefault)
if modelName == "" {
return &UnknownCamera
@ -48,9 +50,9 @@ func NewCamera(modelName string, makeName string) *Camera {
var cameraSlug string
if makeName != "" {
cameraSlug = slug.Make(makeName + " " + modelName)
cameraSlug = slug.Make(txt.Clip(makeName+" "+modelName, txt.ClipSlug))
} else {
cameraSlug = slug.Make(modelName)
cameraSlug = slug.Make(txt.Clip(modelName, txt.ClipSlug))
}
result := &Camera{

View file

@ -17,7 +17,7 @@ var altCountryNames = map[string]string{
// Country represents a country location, used for labeling photos.
type Country struct {
ID string `gorm:"type:varbinary(2);primary_key"`
CountrySlug string `gorm:"type:varbinary(128);unique_index;"`
CountrySlug string `gorm:"type:varbinary(255);unique_index;"`
CountryName string
CountryDescription string `gorm:"type:text;"`
CountryNotes string `gorm:"type:text;"`

View file

@ -11,10 +11,10 @@ type Description struct {
PhotoDescription string `gorm:"type:text;" json:"PhotoDescription"`
PhotoKeywords string `gorm:"type:text;" json:"PhotoKeywords"`
PhotoNotes string `gorm:"type:text;" json:"PhotoNotes"`
PhotoSubject string `json:"PhotoSubject"`
PhotoArtist string `json:"PhotoArtist"`
PhotoCopyright string `json:"PhotoCopyright"`
PhotoLicense string `json:"PhotoLicense"`
PhotoSubject string `gorm:"type:varchar(255);" json:"PhotoSubject"`
PhotoArtist string `gorm:"type:varchar(255);" json:"PhotoArtist"`
PhotoCopyright string `gorm:"type:varchar(255);" json:"PhotoCopyright"`
PhotoLicense string `gorm:"type:varchar(255);" json:"PhotoLicense"`
}
// FirstOrCreate returns the matching entity or creates a new one.

View file

@ -10,7 +10,7 @@ import (
// Event defines temporal event that can be used to link photos together
type Event struct {
EventUUID string `gorm:"type:varbinary(36);unique_index;"`
EventSlug string `gorm:"type:varbinary(128);unique_index;"`
EventSlug string `gorm:"type:varbinary(255);unique_index;"`
EventName string
EventType string
EventDescription string `gorm:"type:text;"`

View file

@ -17,8 +17,8 @@ type File struct {
PhotoID uint `gorm:"index;"`
PhotoUUID string `gorm:"type:varbinary(36);index;"`
FileUUID string `gorm:"type:varbinary(36);unique_index;"`
FileName string `gorm:"type:varbinary(600);unique_index"`
OriginalName string `gorm:"type:varbinary(600);"`
FileName string `gorm:"type:varbinary(768);unique_index"`
OriginalName string `gorm:"type:varbinary(768);"`
FileHash string `gorm:"type:varbinary(128);index"`
FileModified time.Time
FileSize int64
@ -34,9 +34,9 @@ type File struct {
FileHeight int
FileOrientation int
FileAspectRatio float32 `gorm:"type:FLOAT;"`
FileMainColor string `gorm:"type:varbinary(16);index;"`
FileColors string `gorm:"type:binary(9);"`
FileLuminance string `gorm:"type:binary(9);"`
FileMainColor string `gorm:"type:varbinary(16);index;"`
FileColors string `gorm:"type:binary(9);"`
FileLuminance string `gorm:"type:binary(9);"`
FileDiff uint32
FileChroma uint8
FileNotes string `gorm:"type:text"`

View file

@ -18,7 +18,7 @@ const (
type FileShare struct {
FileID uint `gorm:"primary_key;auto_increment:false"`
AccountID uint `gorm:"primary_key;auto_increment:false"`
RemoteName string `gorm:"primary_key;auto_increment:false;type:varbinary(256)"`
RemoteName string `gorm:"primary_key;auto_increment:false;type:varbinary(255)"`
Status string `gorm:"type:varbinary(16);"`
Error string `gorm:"type:varbinary(512);"`
Errors int

View file

@ -17,7 +17,7 @@ const (
// FileSync represents a one-to-many relation between File and Account for syncing with remote services.
type FileSync struct {
RemoteName string `gorm:"primary_key;auto_increment:false;type:varbinary(256)"`
RemoteName string `gorm:"primary_key;auto_increment:false;type:varbinary(255)"`
AccountID uint `gorm:"primary_key;auto_increment:false"`
FileID uint `gorm:"index;"`
RemoteDate time.Time

View file

@ -5,6 +5,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/txt"
)
// Keyword used for full text search
@ -16,7 +17,7 @@ type Keyword struct {
// NewKeyword registers a new keyword in database
func NewKeyword(keyword string) *Keyword {
keyword = strings.ToLower(strings.TrimSpace(keyword))
keyword = strings.ToLower(txt.Clip(keyword, txt.ClipKeyword))
result := &Keyword{
Keyword: keyword,

View file

@ -1,7 +1,6 @@
package entity
import (
"strings"
"time"
"github.com/gosimple/slug"
@ -16,9 +15,9 @@ import (
type Label struct {
ID uint `gorm:"primary_key"`
LabelUUID string `gorm:"type:varbinary(36);unique_index;"`
LabelSlug string `gorm:"type:varbinary(128);unique_index;"`
CustomSlug string `gorm:"type:varbinary(128);index;"`
LabelName string `gorm:"type:varchar(128);"`
LabelSlug string `gorm:"type:varbinary(255);unique_index;"`
CustomSlug string `gorm:"type:varbinary(255);index;"`
LabelName string `gorm:"type:varchar(255);"`
LabelPriority int
LabelFavorite bool
LabelDescription string `gorm:"type:text;"`
@ -42,21 +41,21 @@ func (m *Label) BeforeCreate(scope *gorm.Scope) error {
}
// NewLabel creates a label in database with a given name and priority
func NewLabel(labelName string, labelPriority int) *Label {
labelName = strings.TrimSpace(labelName)
func NewLabel(name string, priority int) *Label {
labelName := txt.Clip(name, txt.ClipDefault)
if labelName == "" {
labelName = "Unknown"
}
labelSlug := slug.Make(labelName)
labelName = txt.Title(txt.Clip(labelName, 128))
labelName = txt.Title(labelName)
labelSlug := slug.Make(txt.Clip(labelName, txt.ClipSlug))
result := &Label{
LabelSlug: labelSlug,
CustomSlug: labelSlug,
LabelName: labelName,
LabelPriority: labelPriority,
LabelPriority: priority,
}
return result
@ -79,18 +78,16 @@ func (m *Label) AfterCreate(scope *gorm.Scope) error {
return scope.SetColumn("New", true)
}
// Rename an existing label
func (m *Label) Rename(name string) {
name = strings.TrimSpace(name)
// SetName changes the label name.
func (m *Label) SetName(name string) {
newName := txt.Clip(name, txt.ClipDefault)
if name == "" {
if newName == "" {
return
}
name = txt.Title(txt.Clip(name, 128))
m.LabelName = name
m.CustomSlug = slug.Make(name)
m.LabelName = txt.Title(newName)
m.CustomSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
}
// Updates a label if necessary
@ -111,7 +108,7 @@ func (m *Label) Update(label classify.Label, db *gorm.DB) error {
}
if m.CustomSlug == m.LabelSlug && label.Title() != m.LabelName {
m.Rename(label.Title())
m.SetName(label.Title())
save = true
}

View file

@ -12,7 +12,7 @@ import (
// Lens represents camera lens (as extracted from UpdateExif metadata)
type Lens struct {
ID uint `gorm:"primary_key"`
LensSlug string `gorm:"type:varbinary(128);unique_index;"`
LensSlug string `gorm:"type:varbinary(255);unique_index;"`
LensModel string
LensMake string
LensType string

View file

@ -9,8 +9,8 @@ import (
// Link represents a sharing link.
type Link struct {
LinkToken string `gorm:"type:varbinary(256);primary_key;"`
LinkPassword string `gorm:"type:varbinary(256);"`
LinkToken string `gorm:"type:varbinary(255);primary_key;"`
LinkPassword string `gorm:"type:varbinary(255);"`
LinkExpires *time.Time `gorm:"type:datetime;"`
ShareUUID string `gorm:"type:varbinary(36);index;"`
CanComment bool

View file

@ -15,7 +15,7 @@ type Location struct {
ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;"`
PlaceID string `gorm:"type:varbinary(16);"`
Place *Place
LocName string `gorm:"type:varchar(128);"`
LocName string `gorm:"type:varchar(255);"`
LocCategory string `gorm:"type:varchar(64);"`
LocSource string `gorm:"type:varbinary(16);"`
CreatedAt time.Time

View file

@ -21,9 +21,9 @@ type Photo struct {
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uuid;" json:"TakenAt"`
TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc"`
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;"`
PhotoPath string `gorm:"type:varbinary(512);index;"`
PhotoPath string `gorm:"type:varbinary(768);index;"`
PhotoName string `gorm:"type:varbinary(255);"`
PhotoTitle string `gorm:"type:varchar(200);" json:"PhotoTitle"`
PhotoTitle string `gorm:"type:varchar(255);" json:"PhotoTitle"`
TitleSrc string `gorm:"type:varbinary(8);" json:"TitleSrc"`
PhotoQuality int `gorm:"type:SMALLINT" json:"PhotoQuality"`
PhotoResolution int `gorm:"type:SMALLINT" json:"PhotoResolution"`
@ -38,7 +38,7 @@ type Photo struct {
PhotoFNumber float32 `gorm:"type:FLOAT;" json:"PhotoFNumber"`
PhotoExposure string `gorm:"type:varbinary(64);" json:"PhotoExposure"`
CameraID uint `gorm:"index:idx_photos_camera_lens;" json:"CameraID"`
CameraSerial string `gorm:"type:varbinary(128);" json:"CameraSerial"`
CameraSerial string `gorm:"type:varbinary(255);" json:"CameraSerial"`
CameraSrc string `gorm:"type:varbinary(8);" json:"CameraSrc"`
LensID uint `gorm:"index:idx_photos_camera_lens;" json:"LensID"`
PlaceID string `gorm:"type:varbinary(16);index;default:'zz'" json:"PlaceID"`
@ -322,8 +322,6 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
return errors.New("photo: won't update title, was modified")
}
m.TitleSrc = SrcAuto
hasLocation := m.Location != nil && m.Location.Place != nil
if hasLocation {
@ -332,34 +330,34 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
if title := labels.Title(loc.Name()); title != "" { // TODO: User defined title format
log.Infof("photo: using label \"%s\" to create photo title", title)
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006"))
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
} else {
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006"))
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006")), SrcAuto)
}
} else if loc.Name() != "" && loc.City() != "" {
if len(loc.Name()) > 45 {
m.PhotoTitle = txt.Title(loc.Name())
m.SetTitle(txt.Title(loc.Name()), SrcAuto)
} else if len(loc.Name()) > 20 || len(loc.City()) > 16 || strings.Contains(loc.Name(), loc.City()) {
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006"))
m.SetTitle(fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006")), SrcAuto)
} else {
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006"))
m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006")), SrcAuto)
}
} else if loc.City() != "" && loc.CountryName() != "" {
if len(loc.City()) > 20 {
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006"))
m.SetTitle(fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006")), SrcAuto)
} else {
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006"))
m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
}
}
}
if !hasLocation || m.NoTitle() {
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
m.PhotoTitle = fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006"))
m.SetTitle(fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006")), SrcAuto)
} else if !m.TakenAtLocal.IsZero() {
m.PhotoTitle = fmt.Sprintf("Unknown / %s", m.TakenAtLocal.Format("2006"))
m.SetTitle(fmt.Sprintf("Unknown / %s", m.TakenAtLocal.Format("2006")), SrcAuto)
} else {
m.PhotoTitle = "Unknown"
m.SetTitle("Unknown", SrcAuto)
}
log.Infof("photo: changed photo title to \"%s\"", m.PhotoTitle)
@ -403,3 +401,15 @@ func (m *Photo) AddLabels(labels classify.Labels, db *gorm.DB) {
db.Set("gorm:auto_preload", true).Model(m).Related(&m.Labels)
}
// SetTitle sets the photo title and clips it to 300 characters.
func (m *Photo) SetTitle(title, source string) {
newTitle := txt.Clip(title, txt.ClipDefault)
if newTitle == "" {
return
}
m.PhotoTitle = newTitle
m.TitleSrc = source
}

View file

@ -69,7 +69,7 @@ func (m *PhotoLabel) Save(db *gorm.DB) error {
}
if m.Label != nil {
m.Label.Rename(m.Label.LabelName)
m.Label.SetName(m.Label.LabelName)
}
return db.Save(m).Error

View file

@ -161,7 +161,7 @@ func Exif(filename string) (data Data, err error) {
if n[0] != "1" && len(n[0]) < len(n[1]) {
n0, _ := strconv.ParseUint(n[0], 10, 64)
if n1, err := strconv.ParseUint(n[1], 10, 64); err == nil && n0 > 0 && n1 > 0 {
value = fmt.Sprintf("1/%d", n1 / n0)
value = fmt.Sprintf("1/%d", n1/n0)
}
}
}

View file

@ -168,8 +168,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
}
if metaData.Title != "" && (photo.NoTitle() || photo.TitleSrc == entity.SrcExif) {
photo.PhotoTitle = txt.Clip(metaData.Title, 200)
photo.TitleSrc = entity.SrcExif
photo.SetTitle(metaData.Title, entity.SrcExif)
}
if metaData.Description != "" && (photo.Description.NoDescription() || photo.DescriptionSrc == entity.SrcExif) {
@ -240,8 +239,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
// TODO: Proof-of-concept for indexing XMP sidecar files
if data, err := meta.XMP(m.FileName()); err == nil {
if data.Title != "" && photo.TitleSrc == entity.SrcAuto {
photo.PhotoTitle = txt.Clip(data.Title, 200)
photo.TitleSrc = entity.SrcXmp
photo.SetTitle(data.Title, entity.SrcXmp)
}
if photo.Description.NoCopyright() && data.Copyright != "" {

View file

@ -9,6 +9,7 @@ import (
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/pluscode"
"github.com/photoprism/photoprism/pkg/s2"
"github.com/photoprism/photoprism/pkg/txt"
)
// GeoResult represents a photo for displaying it on a map.
@ -52,6 +53,8 @@ func (q *Query) Geo(f form.GeoSearch) (results []GeoResult, err error) {
Where("photos.photo_lat <> 0").
Group("photos.id, files.id")
f.Query = txt.Clip(f.Query, txt.ClipKeyword)
if f.Query != "" {
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id").

View file

@ -11,6 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
)
@ -207,7 +208,7 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
if f.Query != "" {
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id").
Where("keywords.keyword LIKE ?", strings.ToLower(f.Query)+"%")
Where("keywords.keyword LIKE ?", strings.ToLower(txt.Clip(f.Query, txt.ClipKeyword))+"%")
}
} else if f.Query != "" {
if len(f.Query) < 2 {
@ -216,7 +217,7 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
slugString := slug.Make(f.Query)
lowerString := strings.ToLower(f.Query)
likeString := lowerString + "%"
likeString := txt.Clip(lowerString, txt.ClipKeyword) + "%"
s = s.Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id").
Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id")

View file

@ -2,12 +2,19 @@ package txt
import "strings"
const (
ClipDefault = 160
ClipSlug = 80
ClipKeyword = 40
)
func Clip(s string, size int) string {
if s == "" {
s = strings.TrimSpace(s)
if s == "" || size <= 0 {
return ""
}
s = strings.TrimSpace(s)
runes := []rune(s)
if len(runes) > size {