Replace date picker with day, month & year inputs #274 #358

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-07-06 07:41:33 +02:00
parent e580a117d6
commit ca154f3bb3
17 changed files with 238 additions and 172 deletions

View file

@ -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()">
</v-select>
</v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-month-select">
@ -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()">
</v-select>
@ -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();

View file

@ -48,61 +48,70 @@
></v-text-field>
</v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-date-select">
<v-flex xs12 sm6 md2 pa-2>
<v-autocomplete
@change="updateTime"
:disabled="disabled"
:label="$gettext('Day')"
hide-details
color="secondary-dark"
v-model="model.Day"
:items="options.Days()"
class="input-day">
</v-autocomplete>
</v-flex>
<v-flex xs12 sm6 md2 pa-2>
<v-autocomplete
@change="updateTime"
:disabled="disabled"
:label="$gettext('Month')"
hide-details
color="secondary-dark"
v-model="model.Month"
:items="options.Months()"
class="input-month">
</v-autocomplete>
</v-flex>
<v-flex xs12 sm6 md2 pa-2>
<v-autocomplete
@change="updateTime"
:disabled="disabled"
:label="$gettext('Year')"
hide-details
color="secondary-dark"
v-model="model.Year"
:items="options.Years()"
class="input-year">
</v-autocomplete>
</v-flex>
<v-flex xs12 sm6 md2 class="pa-2">
<v-text-field
:disabled="disabled"
:value="timeLocalFormatted"
:value="localTime"
browser-autocomplete="off"
:label="labels.localtime"
:label="$gettext('Local Time')"
readonly
hide-details
color="secondary-dark"
class="input-local-time"
></v-text-field>
</v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-date-select">
<v-flex xs12 sm6 md2 pa-2>
<v-text-field
:disabled="disabled"
@change="updateTime"
v-model="time"
:label="labels.utctime"
:label="$gettext('Time UTC')"
hide-details return-masked-value
mask="##:##:##"
color="secondary-dark"
class="input-utc-time"
></v-text-field>
</v-flex>
<v-flex xs12 sm6 md3 class="pa-2 p-date-select">
<v-menu
:disabled="disabled"
:close-on-content-click="false"
full-width
max-width="290"
>
<template v-slot:activator="{ on }">
<v-text-field
:disabled="disabled"
:value="dateFormatted"
browser-autocomplete="off"
:label="labels.utcdate"
readonly
hide-details
v-on="on"
color="secondary-dark"
class="input-utc-date"
></v-text-field>
</template>
<v-date-picker
color="secondary-dark"
v-model="date"
@change="showDatePicker = false"
></v-date-picker>
</v-menu>
</v-flex>
<v-flex xs12 sm6 md3 class="pa-2 p-timezone-select">
<v-flex xs12 sm6 md2 class="pa-2">
<v-autocomplete
@change="updateTime"
:disabled="disabled"
@ -117,6 +126,34 @@
</v-autocomplete>
</v-flex>
<v-flex xs12 sm6 md4 class="pa-2">
<v-autocomplete
:disabled="disabled"
:label="labels.country"
hide-details
browser-autocomplete="off"
color="secondary-dark"
item-value="Code"
item-text="Name"
v-model="model.Country"
:items="countries"
class="input-country">
</v-autocomplete>
</v-flex>
<v-flex xs12 sm6 md2 class="pa-2">
<v-text-field
:disabled="disabled"
hide-details
browser-autocomplete="off"
:label="labels.altitude"
placeholder=""
color="secondary-dark"
v-model="model.Altitude"
class="input-altitude"
></v-text-field>
</v-flex>
<v-flex xs12 sm6 md3 class="pa-2">
<v-text-field
:disabled="disabled"
@ -143,34 +180,6 @@
></v-text-field>
</v-flex>
<v-flex xs12 sm6 md3 class="pa-2">
<v-text-field
:disabled="disabled"
hide-details
browser-autocomplete="off"
:label="labels.altitude"
placeholder=""
color="secondary-dark"
v-model="model.Altitude"
class="input-altitude"
></v-text-field>
</v-flex>
<v-flex xs12 sm6 md3 class="pa-2 p-countries-select">
<v-autocomplete
:disabled="disabled"
:label="labels.country"
hide-details
browser-autocomplete="off"
color="secondary-dark"
item-value="Code"
item-text="Name"
v-model="model.Country"
:items="countries"
class="input-country">
</v-autocomplete>
</v-flex>
<v-flex xs12 md6 pa-2 class="p-camera-select">
<v-select
:disabled="disabled"
@ -420,9 +429,6 @@
language: this.$gettext("Language"),
timezone: this.$gettext("Time Zone"),
title: this.$gettext("Title"),
localtime: this.$gettext("Local Time"),
utctime: this.$gettext("UTC Time"),
utcdate: this.$gettext("UTC Date"),
latitude: this.$gettext("Latitude"),
longitude: this.$gettext("Longitude"),
altitude: this.$gettext("Altitude (m)"),
@ -439,23 +445,18 @@
},
showDatePicker: false,
showTimePicker: false,
date: "",
time: "",
dateFormatted: "",
timeFormatted: "",
timeLocalFormatted: "",
localTime: "",
textRule: v => 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) {

View file

@ -54,35 +54,6 @@
</td>
<td>{{ model.TitleSrc | capitalize }}</td>
</tr>
<tr v-if="model.TakenAcc">
<td>
<translate key="Year">Year</translate>
</td>
<td>
<v-text-field
flat solo dense hide-details v-model="model.Year"
color="secondary-dark"
style="font-weight: 400; font-size: 13px;"
></v-text-field>
</td>
</tr>
<tr v-if="model.TakenAcc">
<td>
<translate key="Month">Month</translate>
</td>
<td>
<v-select
label="Month"
flat solo dense hide-details
color="secondary-dark"
style="font-weight: 400; font-size: 13px;"
item-value="Month"
item-text="Name"
v-model="model.Month"
:items="monthOptions">
</v-select>
</td>
</tr>
<tr>
<td>
<translate key="Quality Score">Quality Score</translate>

View file

@ -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);

View file

@ -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;
};

View file

@ -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:"-"`

View file

@ -25,6 +25,7 @@ const (
// Unknown values.
YearUnknown = -1
MonthUnknown = -1
DayUnknown = -1
TitleUnknown = "Unknown"
// Content types.

View file

@ -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:"-"`

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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:"-"`

View file

@ -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"`

View file

@ -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"`

View file

@ -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()

View file

@ -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 {

View file

@ -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"`

View file

@ -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), ","))
}