diff --git a/frontend/src/component/photo/toolbar.vue b/frontend/src/component/photo/toolbar.vue index 507507e2f..13c2ca2f4 100644 --- a/frontend/src/component/photo/toolbar.vue +++ b/frontend/src/component/photo/toolbar.vue @@ -95,10 +95,10 @@ :label="labels.year" flat solo hide-details color="secondary-dark" - item-value="Year" - item-text="Name" + item-value="value" + item-text="text" v-model="filter.year" - :items="yearOptions"> + :items="yearOptions()"> @@ -106,8 +106,8 @@ :label="labels.month" flat solo hide-details color="secondary-dark" - item-value="Month" - item-text="Name" + item-value="value" + item-text="text" v-model="filter.month" :items="monthOptions()"> @@ -175,7 +175,8 @@ lenses: [{ID: 0, Name: this.$gettext("All Lenses")}], colors: [{Slug: "", Name: this.$gettext("All Colors")}], categories: [{Slug: "", Name: this.$gettext("All Categories")}], - months: [{Month: 0, Name: this.$gettext("All Months")}], + months: [{value: 0, text: this.$gettext("All Months")}], + years: [{value: 0, text: this.$gettext("All Years")}], }, options: { 'views': [ @@ -221,21 +222,6 @@ categoryOptions() { return this.all.categories.concat(this.config.categories); }, - yearOptions() { - let result = [ - {"Year": 0, "Name": this.$gettext("All Years")}, - ]; - - if (this.config.years) { - for (let i = 0; i < this.config.years.length; i++) { - result.push({"Year": this.config.years[i], "Name": this.config.years[i].toString()}); - } - } - - result.push({"Year": -1, "Name": this.$gettext("Unknown")}); - - return result; - }, }, methods: { colorOptions() { @@ -244,6 +230,9 @@ monthOptions() { return this.all.months.concat(options.Months()); }, + yearOptions() { + return this.all.years.concat(options.IndexedYears()); + }, dropdownChange() { this.filterChange(); diff --git a/frontend/src/dialog/photo/details.vue b/frontend/src/dialog/photo/details.vue index b953518c9..9cf5fa802 100644 --- a/frontend/src/dialog/photo/details.vue +++ b/frontend/src/dialog/photo/details.vue @@ -48,61 +48,70 @@ > - + + + + + + + + + + + + + - - + + - - - - - - - + + + + + + + + + + - - - - - - - - - v.length <= this.$config.get('clip') || this.$gettext("Text too long"), }; }, watch: { - date() { - if (!this.date) { - this.dateFormatted = ""; - this.timeLocalFormatted = ""; - return + model() { + if (!this.model.hasId()) { + return; } - this.dateFormatted = DateTime.fromISO(this.date).toLocaleString(DateTime.DATE_FULL); + this.updateTime(); }, }, computed: { @@ -468,29 +469,22 @@ }, methods: { updateTime() { - if (!this.time) { - this.time = DateTime.fromISO(this.model.TakenAt).toUTC().toFormat("HH:mm:ss"); - return; + const isoDate = this.model.isoDate(this.time); + + if(!isoDate) { + return } - if (!this.date) { - return; - } + this.model.TakenAt = isoDate; - this.timeFormatted = DateTime.fromISO(this.time).toLocaleString(DateTime.TIME_24_WITH_SECONDS); + const utcDate = this.model.utcDate(); - const utcDate = this.date + "T" + this.time + "Z"; + this.time = utcDate.toFormat("HH:mm:ss"); - this.model.TakenAt = utcDate; - - this.time = DateTime.fromISO(this.model.TakenAt).toUTC().toFormat("HH:mm:ss"); - - let localDate = DateTime.fromISO(utcDate); + let localDate = utcDate; if (this.model.TimeZone) { localDate = localDate.setZone(this.model.TimeZone); - } else { - localDate = localDate.toUTC(0); } this.model.TakenAtLocal = localDate.toISO({ @@ -498,7 +492,19 @@ includeOffset: false, }) + "Z"; - this.timeLocalFormatted = localDate.toLocaleString(DateTime.TIME_24_WITH_SECONDS); + if(this.model.Day === 0) { + this.model.Day = parseInt(localDate.toFormat("d")); + } + + if(this.model.Month === 0) { + this.model.Month = parseInt(localDate.toFormat("L")); + } + + if(this.model.Year === 0) { + this.model.Year = parseInt(localDate.toFormat("y")); + } + + this.localTime = localDate.toLocaleString(DateTime.TIME_24_WITH_SECONDS); }, left() { this.$emit('next'); @@ -509,21 +515,8 @@ openPhoto() { this.$viewer.show(Thumb.fromFiles([this.model]), 0) }, - refresh(model) { - if (!model.hasId()) return; - - if (model.TakenAt) { - const date = DateTime.fromISO(model.TakenAt).toUTC(); - this.date = date.toISODate(); - this.time = date.toFormat("HH:mm:ss"); - - this.updateTime(); - } - }, save(close) { - if (this.time && this.date) { - this.model.TakenAt = this.date + "T" + this.time + "Z"; - } + this.model.TakenAt = this.model.isoDate(this.time); this.model.update().then(() => { if (close) { diff --git a/frontend/src/dialog/photo/info.vue b/frontend/src/dialog/photo/info.vue index 762a32d26..009c12b65 100644 --- a/frontend/src/dialog/photo/info.vue +++ b/frontend/src/dialog/photo/info.vue @@ -54,35 +54,6 @@ {{ model.TitleSrc | capitalize }} - - - Year - - - - - - - - Month - - - - - - Quality Score diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index 64b4f221b..677b9a74a 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -44,6 +44,7 @@ export const TypeJpeg = "jpg"; export const TypeImage = "image"; export const YearUnknown = -1; export const MonthUnknown = -1; +export const DayUnknown = -1; export class Photo extends RestModel { getDefaults() { @@ -57,7 +58,6 @@ export class Photo extends RestModel { TakenAt: "", TakenAtLocal: "", TakenSrc: "", - TakenAcc: 0, TimeZone: "", Path: "", Color: "", @@ -86,6 +86,7 @@ export class Photo extends RestModel { Country: "", Year: YearUnknown, Month: MonthUnknown, + Day: DayUnknown, Details: { Keywords: "", Notes: "", @@ -124,6 +125,48 @@ export class Photo extends RestModel { }; } + isoDay() { + if(!this.Day || this.Day <= 0) { + return this.TakenAt.substr(8, 2); + } + + return this.Day.toString().padStart(2, "0"); + } + + isoMonth() { + if(!this.Month || this.Month <= 0) { + return this.TakenAt.substr(5, 2); + } + + return this.Month.toString().padStart(2, "0"); + } + + isoYear() { + if(!this.Year || this.Year <= 1000) { + return this.TakenAt.substr(0, 4); + } + + return this.Year.toString(); + } + + isoDate(time) { + if(!this.isoYear()) { + return this.TakenAt; + } + + let date = this.isoYear() + "-" + this.isoMonth() + "-" + this.isoDay(); + + if(!time) { + time = this.TakenAt.substr(11, 8); + } + + return `${date}T${time}Z`; + } + + utcDate() { + return DateTime.fromISO(this.TakenAt).toUTC(); + } + baseName(truncate) { let result = this.fileBase(this.FileName ? this.FileName : this.mainFile().Name); diff --git a/frontend/src/resources/options.js b/frontend/src/resources/options.js index f47e9c845..a5511e3c8 100644 --- a/frontend/src/resources/options.js +++ b/frontend/src/resources/options.js @@ -1,19 +1,60 @@ import {$gettext} from "common/vm"; import moment from "moment-timezone"; import {Info} from "luxon"; +import {config} from "../session"; export const TimeZones = () => moment.tz.names(); +export const Days = () => { + let result = []; + + for (let i = 1; i <= 31; i++) { + result.push({"value": i, "text": i.toString().padStart(2, "0")}); + } + + result.push({"value": -1, "text": $gettext("Unknown")}); + + return result; +}; + +export const Years = () => { + let result = []; + + const currentYear = new Date().getUTCFullYear(); + + for (let i = currentYear; i >= 1750; i--) { + result.push({"value": i, "text": i.toString().padStart(4, "0")}); + } + + result.push({"value": -1, "text": $gettext("Unknown")}); + + return result; +}; + +export const IndexedYears = () => { + let result = []; + + if (config.values.years) { + for (let i = 0; i < config.values.years.length; i++) { + result.push({"value": parseInt(config.values.years[i]), "text": config.values.years[i].toString()}); + } + } + + result.push({"value": -1, "text": $gettext("Unknown")}); + + return result; +}; + export const Months = () => { let result = []; const months = Info.months("long"); for (let i = 0; i < months.length; i++) { - result.push({"Month": i + 1, "Name": months[i]}); + result.push({"value": i + 1, "text": months[i]}); } - result.push({"Month": -1, "Name": $gettext("Unknown")}); + result.push({"value": -1, "text": $gettext("Unknown")}); return result; }; diff --git a/internal/entity/album.go b/internal/entity/album.go index 43a853e00..50828f9b7 100644 --- a/internal/entity/album.go +++ b/internal/entity/album.go @@ -43,6 +43,7 @@ type Album struct { AlbumCountry string `gorm:"type:varbinary(2);index:idx_albums_country_year_month;default:'zz'" json:"Country" yaml:"Country,omitempty"` AlbumYear int `gorm:"index:idx_albums_country_year_month;" json:"Year" yaml:"Year,omitempty"` AlbumMonth int `gorm:"index:idx_albums_country_year_month;" json:"Month" yaml:"Month,omitempty"` + AlbumDay int `json:"Day" yaml:"Day,omitempty"` AlbumFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"` AlbumPrivate bool `json:"Private" yaml:"Private,omitempty"` CreatedAt time.Time `json:"CreatedAt" yaml:"-"` diff --git a/internal/entity/const.go b/internal/entity/const.go index b3ba36e74..4e234890c 100644 --- a/internal/entity/const.go +++ b/internal/entity/const.go @@ -25,6 +25,7 @@ const ( // Unknown values. YearUnknown = -1 MonthUnknown = -1 + DayUnknown = -1 TitleUnknown = "Unknown" // Content types. diff --git a/internal/entity/person.go b/internal/entity/person.go index 2733d18cf..ce68bd886 100644 --- a/internal/entity/person.go +++ b/internal/entity/person.go @@ -38,6 +38,9 @@ type Person struct { CanDownload bool `json:"CanDownload" yaml:"CanDownload,omitempty"` WebDAV bool `gorm:"column:webdav" json:"WebDAV" yaml:"WebDAV,omitempty"` ApiToken string `json:"ApiToken" yaml:"ApiToken,omitempty"` + BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"` + BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"` + BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"` LoginAttempts int `json:"-" yaml:"-,omitempty"` LoginAt *time.Time `json:"-" yaml:"-"` CreatedAt time.Time `json:"CreatedAt" yaml:"-"` diff --git a/internal/entity/photo.go b/internal/entity/photo.go index a792ec7ba..9e0a2b815 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -37,7 +37,6 @@ type Photo struct { TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uid;" json:"TakenAt" yaml:"TakenAt"` TakenAtLocal time.Time `gorm:"type:datetime;" yaml:"-"` TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc" yaml:"TakenSrc,omitempty"` - TakenAcc int `json:"TakenAcc" yaml:"TakenAcc,omitempty"` PhotoUID string `gorm:"type:varbinary(42);unique_index;index:idx_photos_taken_uid;" json:"UID" yaml:"UID"` PhotoType string `gorm:"type:varbinary(8);default:'image';" json:"Type" yaml:"Type"` PhotoTitle string `gorm:"type:varchar(255);" json:"Title" yaml:"Title"` @@ -60,8 +59,9 @@ type Photo struct { PhotoLng float32 `gorm:"type:FLOAT;index;" json:"Lng" yaml:"Lng,omitempty"` PhotoAltitude int `json:"Altitude" yaml:"Altitude,omitempty"` PhotoCountry string `gorm:"type:varbinary(2);index:idx_photos_country_year_month;default:'zz'" json:"Country" yaml:"-"` - PhotoYear int `gorm:"index:idx_photos_country_year_month;" json:"Year" yaml:"-"` - PhotoMonth int `gorm:"index:idx_photos_country_year_month;" json:"Month" yaml:"-"` + PhotoYear int `gorm:"index:idx_photos_country_year_month;" json:"Year" yaml:"Year"` + PhotoMonth int `gorm:"index:idx_photos_country_year_month;" json:"Month" yaml:"Month"` + PhotoDay int `json:"Day" yaml:"Day"` PhotoIso int `json:"Iso" yaml:"ISO,omitempty"` PhotoExposure string `gorm:"type:varbinary(64);" json:"Exposure" yaml:"Exposure,omitempty"` PhotoFNumber float32 `gorm:"type:FLOAT;" json:"FNumber" yaml:"FNumber,omitempty"` @@ -111,7 +111,7 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error { return errors.New("photo: can't save form, id is empty") } - model.UpdateYearMonth() + model.UpdateDateFields() if form.Details.PhotoID == model.ID { if err := deepcopier.Copy(&model.Details).From(form.Details); err != nil { @@ -182,7 +182,7 @@ func (m *Photo) Save() error { labels := m.ClassifyLabels() - m.UpdateYearMonth() + m.UpdateDateFields() if err := m.UpdateTitle(labels); err != nil { log.Info(err) @@ -735,11 +735,11 @@ func (m *Photo) SetTakenAt(taken, local time.Time, zone, source string) { m.TimeZone = zone } - m.UpdateYearMonth() + m.UpdateDateFields() } -// UpdateYearMonth updates internal date fields. -func (m *Photo) UpdateYearMonth() { +// UpdateDateFields updates internal date fields. +func (m *Photo) UpdateDateFields() { if m.TakenAt.IsZero() || m.TakenAt.Year() < 1000 { return } @@ -751,9 +751,11 @@ func (m *Photo) UpdateYearMonth() { if m.TakenSrc == SrcAuto { m.PhotoYear = YearUnknown m.PhotoMonth = MonthUnknown - } else { + m.PhotoDay = DayUnknown + } else if m.TakenSrc != SrcManual { m.PhotoYear = m.TakenAtLocal.Year() m.PhotoMonth = int(m.TakenAtLocal.Month()) + m.PhotoDay = m.TakenAtLocal.Day() } } diff --git a/internal/entity/photo_optimize.go b/internal/entity/photo_optimize.go index ac27d221d..281e569b0 100644 --- a/internal/entity/photo_optimize.go +++ b/internal/entity/photo_optimize.go @@ -105,7 +105,7 @@ func (m *Photo) Optimize() (updated bool, err error) { labels := m.ClassifyLabels() - m.UpdateYearMonth() + m.UpdateDateFields() if err := m.UpdateTitle(labels); err != nil { log.Info(err) diff --git a/internal/form/album_search.go b/internal/form/album_search.go index 011aa293e..f4eedfde8 100644 --- a/internal/form/album_search.go +++ b/internal/form/album_search.go @@ -11,6 +11,7 @@ type AlbumSearch struct { Country string `json:"country"` Year int `json:"year"` Month int `json:"month"` + Day int `json:"day"` Favorite bool `form:"favorite"` Private bool `form:"private"` Count int `form:"count" binding:"required" serialize:"-"` diff --git a/internal/form/photo.go b/internal/form/photo.go index a28324647..1ffb55afe 100644 --- a/internal/form/photo.go +++ b/internal/form/photo.go @@ -22,8 +22,10 @@ type Photo struct { TakenAt time.Time `json:"TakenAt"` TakenAtLocal time.Time `json:"TakenAtLocal"` TakenSrc string `json:"TakenSrc"` - TakenAcc int `json:"TakenAcc"` TimeZone string `json:"TimeZone"` + PhotoYear int `json:"Year"` + PhotoMonth int `json:"Month"` + PhotoDay int `json:"Day"` PhotoTitle string `json:"Title"` TitleSrc string `json:"TitleSrc"` PhotoDescription string `json:"Description"` diff --git a/internal/form/photo_search.go b/internal/form/photo_search.go index 9079d3be2..60541b45c 100644 --- a/internal/form/photo_search.go +++ b/internal/form/photo_search.go @@ -46,6 +46,7 @@ type PhotoSearch struct { State string `form:"state"` // Moments Year int `form:"year"` // Moments Month int `form:"month"` // Moments + Day int `form:"day"` // Moments Color string `form:"color"` Quality int `form:"quality"` Review bool `form:"review"` diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 55e23f6ea..a3f41fa83 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -466,7 +466,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.PlaceID = entity.UnknownPlace.ID } - photo.UpdateYearMonth() + photo.UpdateDateFields() file.FileSidecar = m.IsSidecar() file.FileVideo = m.IsVideo() diff --git a/internal/query/albums.go b/internal/query/albums.go index 6dffc05d5..14c86ecb3 100644 --- a/internal/query/albums.go +++ b/internal/query/albums.go @@ -8,6 +8,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/pkg/capture" + "github.com/photoprism/photoprism/pkg/txt" ) // AlbumResult contains found albums @@ -29,6 +30,7 @@ type AlbumResult struct { AlbumCountry string `json:"Country"` AlbumYear int `json:"Year"` AlbumMonth int `json:"Month"` + AlbumDay int `json:"Day"` AlbumFavorite bool `json:"Favorite"` AlbumPrivate bool `json:"Private"` PhotoCount int `json:"PhotoCount"` @@ -138,11 +140,23 @@ func AlbumSearch(f form.AlbumSearch) (results AlbumResults, err error) { s = s.Where("albums.album_favorite = 1") } + if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.YearUnknown { + s = s.Where("albums.album_year = ?", f.Year) + } + + if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.MonthUnknown { + s = s.Where("albums.album_month = ?", f.Month) + } + + if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.DayUnknown { + s = s.Where("albums.album_day = ?", f.Day) + } + switch f.Order { case "slug": s = s.Order("albums.album_favorite DESC, album_slug ASC") default: - s = s.Order("albums.album_favorite DESC, albums.album_year DESC, albums.album_month DESC, albums.album_title, albums.created_at DESC") + s = s.Order("albums.album_favorite DESC, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.created_at DESC") } if f.Count > 0 && f.Count <= MaxResults { diff --git a/internal/query/photo_results.go b/internal/query/photo_results.go index cb67df1f3..aee04830a 100644 --- a/internal/query/photo_results.go +++ b/internal/query/photo_results.go @@ -20,7 +20,6 @@ type PhotoResult struct { TakenAt time.Time `json:"TakenAt"` TakenAtLocal time.Time `json:"TakenAtLocal"` TakenSrc string `json:"TakenSrc"` - TakenAcc int `json:"TakenAcc"` TimeZone string `json:"TimeZone"` PhotoPath string `json:"Path"` PhotoName string `json:"Name"` @@ -29,6 +28,7 @@ type PhotoResult struct { PhotoDescription string `json:"Description"` PhotoYear int `json:"Year"` PhotoMonth int `json:"Month"` + PhotoDay int `json:"Day"` PhotoCountry string `json:"Country"` PhotoFavorite bool `json:"Favorite"` PhotoPrivate bool `json:"Private"` diff --git a/internal/query/photo_search.go b/internal/query/photo_search.go index 27e50f543..cc855d1d1 100644 --- a/internal/query/photo_search.go +++ b/internal/query/photo_search.go @@ -178,6 +178,10 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error s = s.Where("photos.photo_month = ?", f.Month) } + if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.DayUnknown { + s = s.Where("photos.photo_day = ?", f.Day) + } + if f.Color != "" { s = s.Where("files.file_main_color IN (?)", strings.Split(strings.ToLower(f.Color), ",")) }