Refactor string clipping in frontend & backend
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
b9a82d098e
commit
882340a14c
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
loading: false,
|
||||
search: null,
|
||||
items: [],
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
readonly: this.$config.get("readonly"),
|
||||
active: this.tab,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
total: 0,
|
||||
completed: 0,
|
||||
started: 0,
|
||||
safe: !this.$config.getValue("uploadNSFW")
|
||||
safe: !this.$config.get("uploadNSFW")
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
readonly: this.$config.get("readonly"),
|
||||
active: this.tab,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
readonly: this.$config.get("readonly"),
|
||||
active: this.tab,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
total: 0,
|
||||
completed: 0,
|
||||
started: 0,
|
||||
safe: !this.$config.getValue("uploadNSFW")
|
||||
safe: !this.$config.get("uploadNSFW")
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
readonly: this.$config.get("readonly"),
|
||||
active: this.tab,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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;"`
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;"`
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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").
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue