Moments: Change default sort order in the overview to "newest" #3280

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-03-13 22:17:23 +01:00
parent 0d49ed43f2
commit cc97759806
15 changed files with 121 additions and 82 deletions

View file

@ -160,7 +160,7 @@ export default [
path: "/moments",
component: Albums,
meta: { title: $gettext("Moments"), auth: true },
props: { view: "moment", defaultOrder: "moment", staticFilter: { type: "moment" } },
props: { view: "moment", defaultOrder: "newest", staticFilter: { type: "moment" } },
},
{
name: "moment",

View file

@ -173,11 +173,6 @@ export default {
this.searchExpanded = !this.searchExpanded;
this.growDesc = !this.growDesc;
},
updateAlbum() {
if (this.album.wasChanged()) {
this.album.update();
}
},
setView(name) {
if (name) {
this.refresh({'view': name});

View file

@ -821,11 +821,6 @@ export default {
this.isMini = this.isRestricted;
}
},
createAlbum() {
let name = "New Album";
const album = new Album({Title: name, Favorite: false});
album.save();
},
toggleIsMini() {
this.isMini = !this.isMini;
localStorage.setItem('last_navigation_mode', `${this.isMini}`);

View file

@ -176,6 +176,7 @@ export default {
}
this.model.update().then((m) => {
this.$notify.success(this.$gettext("Changes successfully saved"));
this.categories = this.$config.albumCategories();
this.$emit('close');
});

View file

@ -217,7 +217,11 @@ export default {
this.loading = true;
this.model.updateLink(link).finally(() => this.loading = false);
this.model.updateLink(link).then(() => {
this.$notify.success(this.$gettext("Changes successfully saved"));
}).finally(() => {
this.loading = false;
});
},
remove(index) {
const link = this.links[index];
@ -230,6 +234,7 @@ export default {
this.loading = true;
this.model.removeLink(link).then(() => {
this.$notify.success(this.$gettext("Changes successfully saved"));
this.links.splice(index, 1);
}).finally(() => this.loading = false);
},

View file

@ -343,7 +343,6 @@ export default {
{value: 'favorites', text: this.$gettext('Favorites')},
{value: 'name', text: this.$gettext('Name')},
{value: 'place', text: this.$gettext('Location')},
{value: 'moment', text: this.$gettext('Place & Time')},
{value: 'newest', text: this.$gettext('Newest First')},
{value: 'oldest', text: this.$gettext('Oldest First')},
{value: 'added', text: this.$gettext('Recently Added')},
@ -777,19 +776,32 @@ export default {
this.loadMore();
},
create() {
// Use month and year as default title.
let title = DateTime.local().toFormat("LLLL yyyy");
// Add suffix if the album title already exists.
if (this.results.findIndex(a => a.Title.startsWith(title)) !== -1) {
const existing = this.results.filter(a => a.Title.startsWith(title));
title = `${title} (${existing.length + 1})`;
const re = new RegExp(`${title} \\((\\d?)\\)`, 'i');
let i = 1;
this.results.forEach(a => {
const found = a.Title.match(re);
if (found[1]) {
const n = parseInt(found[1]);
if (n > i) {
i = n;
}
}
});
title = `${title} (${i + 1})`;
}
const album = new Album({"Title": title, "Favorite": false});
album.save();
album.save().then(() => this.$notify.success(this.$gettext("Album created")));
},
onSave(album) {
album.update();
album.update().then(() => this.$notify.success(this.$gettext("Changes successfully saved")));
},
addSelection(uid) {
const pos = this.selection.indexOf(uid);

View file

@ -94,18 +94,13 @@ func CreateAlbum(router *gin.RouterGroup) {
AbortUnexpected(c)
return
}
event.SuccessMsg(i18n.MsgAlbumCreated)
} else {
// Exists, restore if necessary.
a = found
if !a.Deleted() {
event.InfoMsg(i18n.ErrAlreadyExists, a.Title())
c.JSON(http.StatusOK, a)
return
} else if err := a.Restore(); err == nil {
event.SuccessMsg(i18n.MsgRestored, a.Title())
} else {
} else if err := a.Restore(); err != nil {
// Report unexpected error.
log.Errorf("album: %s (restore)", err)
AbortUnexpected(c)
@ -167,8 +162,6 @@ func UpdateAlbum(router *gin.RouterGroup) {
UpdateClientConfig()
event.SuccessMsg(i18n.MsgAlbumSaved)
// PublishAlbumEvent(EntityUpdated, uid, c)
SaveAlbumAsYaml(a)
@ -221,8 +214,6 @@ func DeleteAlbum(router *gin.RouterGroup) {
SaveAlbumAsYaml(a)
event.SuccessMsg(i18n.MsgAlbumDeleted, clean.Log(a.AlbumTitle))
c.JSON(http.StatusOK, a)
})
}

View file

@ -8,9 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
@ -59,8 +57,6 @@ func UpdateLink(c *gin.Context) {
UpdateClientConfig()
event.SuccessMsg(i18n.MsgAlbumSaved)
PublishAlbumEvent(EntityUpdated, link.ShareUID, c)
c.JSON(http.StatusOK, link)
@ -86,8 +82,6 @@ func DeleteLink(c *gin.Context) {
UpdateClientConfig()
event.SuccessMsg(i18n.MsgAlbumSaved)
PublishAlbumEvent(EntityUpdated, link.ShareUID, c)
c.JSON(http.StatusOK, link)
@ -138,8 +132,6 @@ func CreateLink(c *gin.Context) {
UpdateClientConfig()
event.SuccessMsg(i18n.MsgAlbumSaved)
PublishAlbumEvent(EntityUpdated, link.ShareUID, c)
c.JSON(http.StatusOK, link)

View file

@ -423,7 +423,7 @@ func (m *Album) IsDefault() bool {
}
// SetTitle changes the album name.
func (m *Album) SetTitle(title string) {
func (m *Album) SetTitle(title string) *Album {
title = strings.Trim(title, "_&|{}<>: \n\r\t\\")
title = strings.ReplaceAll(title, "\"", "“")
title = txt.Shorten(title, txt.ClipDefault, txt.Ellipsis)
@ -445,10 +445,26 @@ func (m *Album) SetTitle(title string) {
if m.AlbumSlug == "" {
m.AlbumSlug = "-"
}
return m
}
// UpdateSlug updates title and slug of generated albums if needed.
func (m *Album) UpdateSlug(title, slug string) error {
// SetLocation sets a new album location.
func (m *Album) SetLocation(location, state, country string) *Album {
if location != "" {
m.AlbumLocation = txt.Shorten(location, txt.ClipDefault, txt.Ellipsis)
}
if state != "" || country != "" && country != "zz" {
m.AlbumCountry = txt.Clip(country, txt.ClipCountry)
m.AlbumState = txt.Clip(clean.State(state, country), txt.ClipCategory)
}
return m
}
// UpdateTitleAndLocation updates title, location, and slug of generated albums if needed.
func (m *Album) UpdateTitleAndLocation(title, location, state, country, slug string) error {
title = txt.Clip(title, txt.ClipDefault)
slug = txt.Clip(slug, txt.ClipSlug)
@ -463,7 +479,7 @@ func (m *Album) UpdateSlug(title, slug string) error {
changed = true
}
if !changed {
if !changed && state == m.AlbumState && country == m.AlbumCountry {
return nil
}
@ -471,11 +487,27 @@ func (m *Album) UpdateSlug(title, slug string) error {
m.SetTitle(title)
}
return m.Updates(Values{"album_title": m.AlbumTitle, "album_slug": m.AlbumSlug})
// Skip location?
if location == "" && state == "" && (country == "" || country == "zz") {
return m.Updates(Values{
"album_title": m.AlbumTitle,
"album_slug": m.AlbumSlug,
})
}
m.SetLocation(location, state, country)
return m.Updates(Values{
"album_title": m.AlbumTitle,
"album_location": m.AlbumLocation,
"album_state": m.AlbumState,
"album_country": m.AlbumCountry,
"album_slug": m.AlbumSlug,
})
}
// UpdateState updates the album location.
func (m *Album) UpdateState(title, slug, stateName, countryCode string) error {
// UpdateTitleAndState updates the album location.
func (m *Album) UpdateTitleAndState(title, slug, stateName, countryCode string) error {
title = txt.Clip(title, txt.ClipDefault)
slug = txt.Clip(slug, txt.ClipSlug)
@ -536,7 +568,6 @@ func (m *Album) SaveForm(f form.Album) error {
// Update sets a new value for a database column.
func (m *Album) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).Update(attr, value).Error
}

View file

@ -83,7 +83,7 @@ func TestAlbum_UpdateSlug(t *testing.T) {
t.Fatal(err)
}
if err := album.UpdateSlug("November / 2002", "november-2002"); err != nil {
if err := album.UpdateTitleAndLocation("November / 2002", "", "", "", "november-2002"); err != nil {
t.Fatal(err)
}
@ -110,7 +110,7 @@ func TestAlbum_UpdateState(t *testing.T) {
t.Fatal(err)
}
if err := album.UpdateState("Alberta", "canada-alberta", "Alberta", "ca"); err != nil {
if err := album.UpdateTitleAndState("Alberta", "canada-alberta", "Alberta", "ca"); err != nil {
t.Fatal(err)
}

View file

@ -125,7 +125,7 @@ func (w *Moments) Start() (err error) {
} else {
for _, mom := range results {
if a := entity.FindMonthAlbum(mom.Year, mom.Month); a != nil {
if err := a.UpdateSlug(mom.Title(), mom.Slug()); err != nil {
if err := a.UpdateTitleAndLocation(mom.Title(), "", "", "", mom.Slug()); err != nil {
log.Errorf("moments: %s (update slug)", err.Error())
}
@ -158,7 +158,7 @@ func (w *Moments) Start() (err error) {
}
if a := entity.FindAlbumByAttr(S{mom.Slug(), mom.TitleSlug()}, S{f.Serialize()}, entity.AlbumMoment); a != nil {
if err := a.UpdateSlug(mom.Title(), mom.Slug()); err != nil {
if err := a.UpdateTitleAndLocation(mom.Title(), "", mom.State, mom.Country, mom.Slug()); err != nil {
log.Errorf("moments: %s (update slug)", err.Error())
}
@ -170,7 +170,7 @@ func (w *Moments) Start() (err error) {
}
} else if a := entity.NewMomentsAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
a.AlbumYear = mom.Year
a.AlbumCountry = mom.Country
a.SetLocation("", mom.State, mom.Country)
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err)
@ -193,7 +193,7 @@ func (w *Moments) Start() (err error) {
}
if a := entity.FindAlbumByAttr(S{mom.Slug(), mom.TitleSlug()}, S{f.Serialize()}, entity.AlbumState); a != nil {
if err := a.UpdateState(mom.Title(), mom.Slug(), mom.State, mom.Country); err != nil {
if err := a.UpdateTitleAndState(mom.Title(), mom.Slug(), mom.State, mom.Country); err != nil {
log.Errorf("moments: %s (update state)", err.Error())
}
@ -205,9 +205,7 @@ func (w *Moments) Start() (err error) {
log.Infof("moments: %s restored", clean.Log(a.AlbumTitle))
}
} else if a := entity.NewStateAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
a.AlbumLocation = mom.CountryName()
a.AlbumCountry = mom.Country
a.AlbumState = mom.State
a.SetLocation(mom.CountryName(), mom.State, mom.Country)
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err)
@ -231,7 +229,7 @@ func (w *Moments) Start() (err error) {
}
if a := entity.FindAlbumByAttr(S{mom.Slug(), mom.TitleSlug()}, S{f.Serialize()}, entity.AlbumMoment); a != nil {
if err := a.UpdateSlug(mom.Title(), mom.Slug()); err != nil {
if err := a.UpdateTitleAndLocation(mom.Title(), "", "", "", mom.Slug()); err != nil {
log.Errorf("moments: %s (update slug)", err.Error())
}

View file

@ -14,10 +14,10 @@ import (
// Moment contains photo counts per month and year
type Moment struct {
Label string `json:"Label"`
Country string `json:"Country"`
State string `json:"State"`
Year int `json:"Year"`
Month int `json:"Month"`
State string `json:"State"`
Country string `json:"Country"`
PhotoCount int `json:"PhotoCount"`
}
@ -103,6 +103,15 @@ func (m Moment) CountryName() string {
return maps.CountryName(m.Country)
}
// Location returns the location name for the moment.
func (m Moment) Location() string {
if state := clean.State(m.State, m.Country); state != "" {
return fmt.Sprintf("%s, %s", state, m.CountryName())
}
return m.CountryName()
}
// Slug returns an identifier string for a moment.
func (m Moment) Slug() (s string) {
state := clean.State(m.State, m.Country)
@ -201,7 +210,7 @@ func MomentsTime(threshold int, public bool) (results Moments, err error) {
// MomentsCountries returns the most popular countries by year.
func MomentsCountries(threshold int, public bool) (results Moments, err error) {
db := UnscopedDb().Table("photos").
Select("photo_country AS country, photo_year AS year, COUNT(*) AS photo_count ").
Select("photo_year AS year, photo_country AS country, COUNT(*) AS photo_count").
Where("photos.photo_quality >= 3 AND deleted_at IS NULL AND photo_country <> 'zz' AND photo_year > 0")
// Ignore private pictures?
@ -209,7 +218,7 @@ func MomentsCountries(threshold int, public bool) (results Moments, err error) {
db = db.Where("photo_private = 0")
}
db = db.Group("photo_country, photo_year").
db = db.Group("photo_year, photo_country").
Having("photo_count >= ?", threshold)
if err := db.Scan(&results).Error; err != nil {

View file

@ -31,7 +31,7 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
// Base query.
s := UnscopedDb().Table("albums").
Select("albums.*, cp.photo_count, cl.link_count, CASE WHEN albums.album_year = 0 THEN 0 ELSE 1 END AS has_year").
Select("albums.*, cp.photo_count, cl.link_count, CASE WHEN albums.album_year = 0 THEN 0 ELSE 1 END AS has_year, CASE WHEN albums.album_location = '' THEN 1 ELSE 0 END AS no_location").
Joins("LEFT JOIN (SELECT album_uid, count(photo_uid) AS photo_count FROM photos_albums WHERE hidden = 0 AND missing = 0 GROUP BY album_uid) AS cp ON cp.album_uid = albums.album_uid").
Joins("LEFT JOIN (SELECT share_uid, count(share_uid) AS link_count FROM links GROUP BY share_uid) AS cl ON cl.share_uid = albums.album_uid").
Where("albums.deleted_at IS NULL")
@ -84,15 +84,19 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
switch f.Order {
case sortby.Count:
s = s.Order("photo_count DESC, albums.album_title, albums.album_uid DESC")
case sortby.Newest:
case sortby.Moment, sortby.Newest:
if f.Type == entity.AlbumDefault || f.Type == entity.AlbumState {
s = s.Order("albums.album_uid DESC")
} else if f.Type == entity.AlbumMoment {
s = s.Order("has_year, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC")
} else {
s = s.Order("albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC")
}
case sortby.Oldest:
if f.Type == entity.AlbumDefault || f.Type == entity.AlbumState {
s = s.Order("albums.album_uid ASC")
} else if f.Type == entity.AlbumMoment {
s = s.Order("has_year, albums.album_year ASC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid ASC")
} else {
s = s.Order("albums.album_year ASC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid ASC")
}
@ -100,10 +104,8 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
s = s.Order("albums.album_uid DESC")
case sortby.Edited:
s = s.Order("albums.updated_at DESC, albums.album_uid DESC")
case sortby.Moment:
s = s.Order("albums.album_favorite DESC, has_year, albums.album_year DESC, albums.album_month DESC, albums.album_title ASC, albums.album_uid DESC")
case sortby.Place:
s = s.Order("albums.album_location, albums.album_title, albums.album_year DESC, albums.album_month ASC, albums.album_day ASC, albums.album_uid DESC")
s = s.Order("no_location, albums.album_location, has_year, albums.album_year DESC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid DESC")
case sortby.Path:
s = s.Order("albums.album_path, albums.album_uid DESC")
case sortby.Category:
@ -124,6 +126,12 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
} else {
s = s.Order("albums.album_title ASC, albums.album_uid DESC")
}
case sortby.NameReverse:
if f.Type == entity.AlbumFolder {
s = s.Order("albums.album_path DESC, albums.album_uid DESC")
} else {
s = s.Order("albums.album_title DESC, albums.album_uid DESC")
}
default:
s = s.Order("albums.album_favorite DESC, albums.album_title ASC, albums.album_uid DESC")
}

View file

@ -1,24 +1,25 @@
package sortby
const (
Default = ""
Relevance = "relevance"
Duration = "duration"
Size = "size"
Count = "count"
Added = "added"
Imported = "imported"
Edited = "edited"
Newest = "newest"
Oldest = "oldest"
Place = "place"
Moment = "moment"
Favorites = "favorites"
Name = "name"
Path = "path"
Slug = "slug"
Category = "category"
Similar = "similar"
Random = "random"
Invalid = "invalid"
Default = ""
Relevance = "relevance"
Duration = "duration"
Size = "size"
Count = "count"
Added = "added"
Imported = "imported"
Edited = "edited"
Newest = "newest"
Oldest = "oldest"
Place = "place"
Moment = "moment"
Favorites = "favorites"
Name = "name"
NameReverse = "name_reverse"
Path = "path"
Slug = "slug"
Category = "category"
Similar = "similar"
Random = "random"
Invalid = "invalid"
)

View file

@ -6,6 +6,7 @@ import (
const (
Ellipsis = "…"
ClipCountry = 2
ClipRole = 32
ClipKeyword = 40
ClipIP = 48