1
Fork 0

Merge pull request #530 from photoview/improved-timeline

Improved timeline
This commit is contained in:
Viktor Strate Kløvedal 2021-09-25 18:21:56 +02:00 committed by GitHub
commit 77e1a45f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 2450 additions and 1118 deletions

3
.gitignore vendored
View File

@ -37,6 +37,7 @@ yarn-debug.log*
yarn-error.log*
.eslintcache
# vscode
# IDEs
.vscode
__debug_bin
.idea

View File

@ -1,3 +0,0 @@
{
"eslint.workingDirectories": ["ui"]
}

View File

@ -38,6 +38,8 @@ models:
resolver: true
type:
resolver: true
album:
resolver: true
MediaURL:
model: github.com/photoview/photoview/api/graphql/models.MediaURL
MediaEXIF:

View File

@ -98,6 +98,7 @@ type ComplexityRoot struct {
Media struct {
Album func(childComplexity int) int
Date func(childComplexity int) int
Downloads func(childComplexity int) int
Exif func(childComplexity int) int
Faces func(childComplexity int) int
@ -188,7 +189,7 @@ type ComplexityRoot struct {
MyFaceGroups func(childComplexity int, paginate *models.Pagination) int
MyMedia func(childComplexity int, order *models.Ordering, paginate *models.Pagination) int
MyMediaGeoJSON func(childComplexity int) int
MyTimeline func(childComplexity int, paginate *models.Pagination, onlyFavorites *bool) int
MyTimeline func(childComplexity int, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) int
MyUser func(childComplexity int) int
MyUserPreferences func(childComplexity int) int
Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int
@ -287,11 +288,12 @@ type MediaResolver interface {
Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error)
HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error)
VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error)
Album(ctx context.Context, obj *models.Media) (*models.Album, error)
Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, error)
Favorite(ctx context.Context, obj *models.Media) (bool, error)
Type(ctx context.Context, obj *models.Media) (models.MediaType, error)
Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error)
Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error)
Faces(ctx context.Context, obj *models.Media) ([]*models.ImageFace, error)
@ -332,7 +334,7 @@ type QueryResolver interface {
MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error)
Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error)
MediaList(ctx context.Context, ids []int) ([]*models.Media, error)
MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool) ([]*models.TimelineGroup, error)
MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error)
MyMediaGeoJSON(ctx context.Context) (interface{}, error)
MapboxToken(ctx context.Context) (*string, error)
ShareToken(ctx context.Context, credentials models.ShareTokenCredentials) (*models.ShareToken, error)
@ -567,6 +569,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Media.Album(childComplexity), true
case "Media.date":
if e.complexity.Media.Date == nil {
break
}
return e.complexity.Media.Date(childComplexity), true
case "Media.downloads":
if e.complexity.Media.Downloads == nil {
break
@ -1226,7 +1235,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Query.MyTimeline(childComplexity, args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool)), true
return e.complexity.Query.MyTimeline(childComplexity, args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool), args["fromDate"].(*time.Time)), true
case "Query.myUser":
if e.complexity.Query.MyUser == nil {
@ -1723,7 +1732,15 @@ type Query {
"Get a list of media by their ids, user must own the media or be admin"
mediaList(ids: [ID!]!): [Media!]!
myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! @isAuthorized
"""
Get a list of media, ordered first by day, then by album if multiple media was found for the same day.
"""
myTimeline(
paginate: Pagination,
onlyFavorites: Boolean,
"Only fetch media that is older than this date"
fromDate: Time
): [Media!]! @isAuthorized
"Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! @isAuthorized
@ -1980,6 +1997,8 @@ type Media {
videoMetadata: VideoMetadata
favorite: Boolean!
type: MediaType!
"The date the image was shot or the date it was imported as a fallback"
date: Time!
shares: [ShareToken!]!
downloads: [MediaDownload!]!
@ -2843,6 +2862,15 @@ func (ec *executionContext) field_Query_myTimeline_args(ctx context.Context, raw
}
}
args["onlyFavorites"] = arg1
var arg2 *time.Time
if tmp, ok := rawArgs["fromDate"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fromDate"))
arg2, err = ec.unmarshalOTime2ᚖtimeᚐTime(ctx, tmp)
if err != nil {
return nil, err
}
}
args["fromDate"] = arg2
return args, nil
}
@ -4067,14 +4095,14 @@ func (ec *executionContext) _Media_album(ctx context.Context, field graphql.Coll
Object: "Media",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Album, nil
return ec.resolvers.Media().Album(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -4086,9 +4114,9 @@ func (ec *executionContext) _Media_album(ctx context.Context, field graphql.Coll
}
return graphql.Null
}
res := resTmp.(models.Album)
res := resTmp.(*models.Album)
fc.Result = res
return ec.marshalNAlbum2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
return ec.marshalNAlbum2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
}
func (ec *executionContext) _Media_exif(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) {
@ -4225,6 +4253,41 @@ func (ec *executionContext) _Media_type(ctx context.Context, field graphql.Colle
return ec.marshalNMediaType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaType(ctx, field.Selections, res)
}
func (ec *executionContext) _Media_date(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Media",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Date(), nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(time.Time)
fc.Result = res
return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
}
func (ec *executionContext) _Media_shares(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -7112,7 +7175,7 @@ func (ec *executionContext) _Query_myTimeline(ctx context.Context, field graphql
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().MyTimeline(rctx, args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool))
return ec.resolvers.Query().MyTimeline(rctx, args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool), args["fromDate"].(*time.Time))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAuthorized == nil {
@ -7128,10 +7191,10 @@ func (ec *executionContext) _Query_myTimeline(ctx context.Context, field graphql
if tmp == nil {
return nil, nil
}
if data, ok := tmp.([]*models.TimelineGroup); ok {
if data, ok := tmp.([]*models.Media); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/photoview/photoview/api/graphql/models.TimelineGroup`, tmp)
return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/photoview/photoview/api/graphql/models.Media`, tmp)
})
if err != nil {
ec.Error(ctx, err)
@ -7143,9 +7206,9 @@ func (ec *executionContext) _Query_myTimeline(ctx context.Context, field graphql
}
return graphql.Null
}
res := resTmp.([]*models.TimelineGroup)
res := resTmp.([]*models.Media)
fc.Result = res
return ec.marshalNTimelineGroup2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐTimelineGroup(ctx, field.Selections, res)
return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_myMediaGeoJson(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
@ -10576,10 +10639,19 @@ func (ec *executionContext) _Media(ctx context.Context, sel ast.SelectionSet, ob
return res
})
case "album":
out.Values[i] = ec._Media_album(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Media_album(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "exif":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -10621,6 +10693,11 @@ func (ec *executionContext) _Media(ctx context.Context, sel ast.SelectionSet, ob
}
return res
})
case "date":
out.Values[i] = ec._Media_date(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "shares":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -12466,53 +12543,6 @@ func (ec *executionContext) marshalNTime2timeᚐTime(ctx context.Context, sel as
return res
}
func (ec *executionContext) marshalNTimelineGroup2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐTimelineGroupᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.TimelineGroup) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNTimelineGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐTimelineGroup(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNTimelineGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐTimelineGroup(ctx context.Context, sel ast.SelectionSet, v *models.TimelineGroup) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._TimelineGroup(ctx, sel, v)
}
func (ec *executionContext) marshalNUser2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v models.User) graphql.Marshaler {
return ec._User(ctx, sel, &v)
}

View File

@ -0,0 +1,22 @@
package actions
import (
"github.com/photoview/photoview/api/graphql/models"
"gorm.io/gorm"
)
func MyMedia(db *gorm.DB, user *models.User, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
if err := user.FillAlbums(db); err != nil {
return nil, err
}
query := db.Where("media.album_id IN (SELECT user_albums.album_id FROM user_albums WHERE user_albums.user_id = ?)", user.ID)
query = models.FormatSQL(query, order, paginate)
var media []*models.Media
if err := query.Find(&media).Error; err != nil {
return nil, err
}
return media, nil
}

View File

@ -0,0 +1,88 @@
package actions_test
import (
"testing"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/graphql/models/actions"
"github.com/photoview/photoview/api/test_utils"
"github.com/stretchr/testify/assert"
)
func TestMyMedia(t *testing.T) {
db := test_utils.DatabaseTest(t)
password := "1234"
user, err := models.RegisterUser(db, "user", &password, false)
assert.NoError(t, err)
rootAlbum := models.Album{
Title: "root",
Path: "/photos",
}
assert.NoError(t, db.Save(&rootAlbum).Error)
childAlbum := models.Album{
Title: "subalbum",
Path: "/photos/subalbum",
ParentAlbumID: &rootAlbum.ID,
}
assert.NoError(t, db.Save(&childAlbum).Error)
assert.NoError(t, db.Model(&user).Association("Albums").Append(&rootAlbum))
assert.NoError(t, db.Model(&user).Association("Albums").Append(&childAlbum))
media := []models.Media{
{
Title: "pic1",
Path: "/photos/pic1",
AlbumID: rootAlbum.ID,
},
{
Title: "pic2",
Path: "/photos/pic2",
AlbumID: rootAlbum.ID,
},
{
Title: "pic3",
Path: "/photos/subalbum/pic3",
AlbumID: childAlbum.ID,
},
{
Title: "pic4",
Path: "/photos/subalbum/pic4",
AlbumID: childAlbum.ID,
},
}
assert.NoError(t, db.Save(&media).Error)
anotherUser, err := models.RegisterUser(db, "user2", &password, false)
assert.NoError(t, err)
anotherAlbum := models.Album{
Title: "AnotherAlbum",
Path: "/another",
}
assert.NoError(t, db.Save(&anotherAlbum).Error)
anotherMedia := models.Media{
Title: "anotherPic",
Path: "/another/anotherPic",
AlbumID: anotherAlbum.ID,
}
assert.NoError(t, db.Save(&anotherMedia).Error)
assert.NoError(t, db.Model(&anotherUser).Association("Albums").Append(&anotherAlbum))
t.Run("Simple query", func(t *testing.T) {
myMedia, err := actions.MyMedia(db, user, nil, nil)
assert.NoError(t, err)
assert.Len(t, myMedia, 4)
})
}

View File

@ -45,6 +45,10 @@ func (m *Media) BeforeSave(tx *gorm.DB) error {
return nil
}
func (m *Media) Date() time.Time {
return m.DateShot
}
type MediaType string
const (

View File

@ -8,6 +8,7 @@ import (
api "github.com/photoview/photoview/api/graphql"
"github.com/photoview/photoview/api/graphql/auth"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/graphql/models/actions"
"github.com/photoview/photoview/api/scanner/face_detection"
"github.com/pkg/errors"
"gorm.io/gorm/clause"
@ -19,29 +20,7 @@ func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, pag
return nil, errors.New("unauthorized")
}
if err := user.FillAlbums(r.Database); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
var media []*models.Media
query := r.Database.
Joins("Album").
Where("albums.id IN (?)", userAlbumIDs).
Where("media.id IN (?)", r.Database.Model(&models.MediaURL{}).Select("id").Where("media_url.media_id = media.id"))
query = models.FormatSQL(query, order, paginate)
if err := query.Find(&media).Error; err != nil {
return nil, err
}
return media, nil
return actions.MyMedia(r.Database, user, order, paginate)
}
func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) {
@ -115,6 +94,15 @@ func (r *mediaResolver) Type(ctx context.Context, media *models.Media) (models.M
return formattedType, nil
}
func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.Album, error) {
var album models.Album
err := r.Database.Find(&album, obj.AlbumID).Error
if err != nil {
return nil, err
}
return &album, nil
}
func (r *mediaResolver) Shares(ctx context.Context, media *models.Media) ([]*models.ShareToken, error) {
var shareTokens []*models.ShareToken
if err := r.Database.Where("media_id = ?", media.ID).Find(&shareTokens).Error; err != nil {

View File

@ -2,135 +2,40 @@ package resolvers
import (
"context"
"fmt"
"time"
"github.com/photoview/photoview/api/database"
"github.com/photoview/photoview/api/graphql/auth"
"github.com/photoview/photoview/api/graphql/models"
"gorm.io/gorm"
)
func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool) ([]*models.TimelineGroup, error) {
func (r *queryResolver) MyTimeline(ctx context.Context, paginate *models.Pagination, onlyFavorites *bool, fromDate *time.Time) ([]*models.Media, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
var timelineGroups []*models.TimelineGroup
query := r.Database.
Joins("JOIN albums ON media.album_id = albums.id").
Where("albums.id IN (?)", r.Database.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID)).
Order("YEAR(media.date_shot) DESC").
Order("MONTH(media.date_shot) DESC").
Order("DAY(media.date_shot) DESC").
Order("albums.title ASC")
transactionError := r.Database.Transaction(func(tx *gorm.DB) error {
// album_id, year, month, day
daysQuery := tx.Select(
"albums.id AS album_id",
fmt.Sprintf("%s AS year", database.DateExtract(tx, database.DateCompYear, "media.date_shot")),
fmt.Sprintf("%s AS month", database.DateExtract(tx, database.DateCompMonth, "media.date_shot")),
fmt.Sprintf("%s AS day", database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
).
Table("media").
Joins("JOIN albums ON media.album_id = albums.id").
Where("albums.id IN (?)", tx.Table("user_albums").Select("user_albums.album_id").Where("user_id = ?", user.ID))
if onlyFavorites != nil && *onlyFavorites == true {
daysQuery.Where("media.id IN (?)", tx.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
}
if paginate != nil {
if paginate.Limit != nil {
daysQuery.Limit(*paginate.Limit)
}
if paginate.Offset != nil {
daysQuery.Offset(*paginate.Offset)
}
}
rows, err := daysQuery.Group("albums.id").Group(
fmt.Sprintf("%s, %s, %s",
database.DateExtract(tx, database.DateCompYear, "media.date_shot"),
database.DateExtract(tx, database.DateCompMonth, "media.date_shot"),
database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
).
Order(
fmt.Sprintf("%s DESC, %s DESC, %s DESC",
database.DateExtract(tx, database.DateCompYear, "media.date_shot"),
database.DateExtract(tx, database.DateCompMonth, "media.date_shot"),
database.DateExtract(tx, database.DateCompDay, "media.date_shot")),
).Rows()
defer rows.Close()
if err != nil {
return err
}
type group struct {
albumID int
year int
month int
day int
}
dbGroups := make([]group, 0)
for rows.Next() {
var g group
rows.Scan(&g.albumID, &g.year, &g.month, &g.day)
dbGroups = append(dbGroups, g)
}
timelineGroups = make([]*models.TimelineGroup, len(dbGroups))
for i, group := range dbGroups {
// Fill album
var groupAlbum models.Album
if err := tx.First(&groupAlbum, group.albumID).Error; err != nil {
return err
}
// Fill media
var groupMedia []*models.Media
mediaQuery := tx.Model(&models.Media{}).
Where("album_id = ?", group.albumID).
Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompYear, "media.date_shot")), group.year).
Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompMonth, "media.date_shot")), group.month).
Where(fmt.Sprintf("%s = ?", database.DateExtract(tx, database.DateCompDay, "media.date_shot")), group.day).
Order("date_shot DESC")
if onlyFavorites != nil && *onlyFavorites == true {
mediaQuery.Where("media.id IN (?)", tx.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
}
if err := mediaQuery.Limit(5).Find(&groupMedia).Error; err != nil {
return err
}
// Get total media count
var totalMedia int64
if err := mediaQuery.Count(&totalMedia).Error; err != nil {
return err
}
var date time.Time = groupMedia[0].DateShot
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
timelineGroup := models.TimelineGroup{
Album: &groupAlbum,
Media: groupMedia,
MediaTotal: int(totalMedia),
Date: date,
}
timelineGroups[i] = &timelineGroup
}
return nil
})
if transactionError != nil {
return nil, transactionError
if fromDate != nil {
query = query.Where("media.date_shot < ?", fromDate)
}
return timelineGroups, nil
if onlyFavorites != nil && *onlyFavorites == true {
query = query.Where("media.id IN (?)", r.Database.Table("user_media_data").Select("user_media_data.media_id").Where("user_media_data.user_id = ?", user.ID).Where("user_media_data.favorite = 1"))
}
query = models.FormatSQL(query, nil, paginate)
var media []*models.Media
if err := query.Find(&media).Error; err != nil {
return nil, err
}
return media, nil
}

View File

@ -63,7 +63,15 @@ type Query {
"Get a list of media by their ids, user must own the media or be admin"
mediaList(ids: [ID!]!): [Media!]!
myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! @isAuthorized
"""
Get a list of media, ordered first by day, then by album if multiple media was found for the same day.
"""
myTimeline(
paginate: Pagination,
onlyFavorites: Boolean,
"Only fetch media that is older than this date"
fromDate: Time
): [Media!]! @isAuthorized
"Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! @isAuthorized
@ -318,6 +326,8 @@ type Media {
videoMetadata: VideoMetadata
favorite: Boolean!
type: MediaType!
"The date the image was shot or the date it was imported as a fallback"
date: Time!
shares: [ShareToken!]!
downloads: [MediaDownload!]!

697
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -63,7 +63,7 @@
"lint:types": "tsc --noemit",
"jest": "craco test --setupFilesAfterEnv ./testing/setupTests.ts",
"jest:ci": "CI=true craco test --setupFilesAfterEnv ./testing/setupTests.ts --verbose --ci --coverage",
"genSchemaTypes": "npx apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts",
"genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts",
"extractTranslations": "i18next -c i18next-parser.config.js",
"prepare": "(cd .. && npx husky install)"
},
@ -75,7 +75,9 @@
"husky": "^6.0.0",
"i18next-parser": "^4.2.0",
"lint-staged": "^11.0.1",
"tsc-files": "^1.1.2"
"tsc-files": "^1.1.2",
"apollo": "2.33.4",
"apollo-language-server": "1.26.3"
},
"prettier": {
"trailingComma": "es5",

View File

@ -3,102 +3,102 @@
// @generated
// This file was automatically generated and should not be edited.
import { OrderDirection, MediaType } from './../../../__generated__/globalTypes'
import { OrderDirection, MediaType } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL query operation: albumQuery
// ====================================================
export interface albumQuery_album_subAlbums_thumbnail_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface albumQuery_album_subAlbums_thumbnail {
__typename: 'Media'
id: string
__typename: "Media";
id: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: albumQuery_album_subAlbums_thumbnail_thumbnail | null
thumbnail: albumQuery_album_subAlbums_thumbnail_thumbnail | null;
}
export interface albumQuery_album_subAlbums {
__typename: 'Album'
id: string
title: string
__typename: "Album";
id: string;
title: string;
/**
* An image in this album used for previewing this album
*/
thumbnail: albumQuery_album_subAlbums_thumbnail | null
thumbnail: albumQuery_album_subAlbums_thumbnail | null;
}
export interface albumQuery_album_media_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface albumQuery_album_media_highRes {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface albumQuery_album_media_videoWeb {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface albumQuery_album_media {
__typename: 'Media'
id: string
type: MediaType
__typename: "Media";
id: string;
type: MediaType;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: albumQuery_album_media_thumbnail | null
thumbnail: albumQuery_album_media_thumbnail | null;
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: albumQuery_album_media_highRes | null
highRes: albumQuery_album_media_highRes | null;
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: albumQuery_album_media_videoWeb | null
favorite: boolean
videoWeb: albumQuery_album_media_videoWeb | null;
favorite: boolean;
}
export interface albumQuery_album {
__typename: 'Album'
id: string
title: string
__typename: "Album";
id: string;
title: string;
/**
* The albums contained in this album
*/
subAlbums: albumQuery_album_subAlbums[]
subAlbums: albumQuery_album_subAlbums[];
/**
* The media inside this album
*/
media: albumQuery_album_media[]
media: albumQuery_album_media[];
}
export interface albumQuery {
@ -106,14 +106,14 @@ export interface albumQuery {
* Get album by id, user must own the album or be admin
* If valid tokenCredentials are provided, the album may be retrived without further authentication
*/
album: albumQuery_album
album: albumQuery_album;
}
export interface albumQueryVariables {
id: string
onlyFavorites?: boolean | null
mediaOrderBy?: string | null
mediaOrderDirection?: OrderDirection | null
limit?: number | null
offset?: number | null
id: string;
onlyFavorites?: boolean | null;
mediaOrderBy?: string | null;
mediaOrderDirection?: OrderDirection | null;
limit?: number | null;
offset?: number | null;
}

View File

@ -8,35 +8,35 @@
// ====================================================
export interface getMyAlbums_myAlbums_thumbnail_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface getMyAlbums_myAlbums_thumbnail {
__typename: 'Media'
id: string
__typename: "Media";
id: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null
thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null;
}
export interface getMyAlbums_myAlbums {
__typename: 'Album'
id: string
title: string
__typename: "Album";
id: string;
title: string;
/**
* An image in this album used for previewing this album
*/
thumbnail: getMyAlbums_myAlbums_thumbnail | null
thumbnail: getMyAlbums_myAlbums_thumbnail | null;
}
export interface getMyAlbums {
/**
* List of albums owned by the logged in user.
*/
myAlbums: getMyAlbums_myAlbums[]
myAlbums: getMyAlbums_myAlbums[];
}

View File

@ -8,13 +8,13 @@
// ====================================================
export interface CheckInitialSetup_siteInfo {
__typename: 'SiteInfo'
__typename: "SiteInfo";
/**
* Whether or not the initial setup wizard should be shown
*/
initialSetup: boolean
initialSetup: boolean;
}
export interface CheckInitialSetup {
siteInfo: CheckInitialSetup_siteInfo
siteInfo: CheckInitialSetup_siteInfo;
}

View File

@ -3,80 +3,80 @@
// @generated
// This file was automatically generated and should not be edited.
import { MediaType } from './../../../../__generated__/globalTypes'
import { MediaType } from "./../../../../__generated__/globalTypes";
// ====================================================
// GraphQL query operation: singleFaceGroup
// ====================================================
export interface singleFaceGroup_faceGroup_imageFaces_rectangle {
__typename: 'FaceRectangle'
minX: number
maxX: number
minY: number
maxY: number
__typename: "FaceRectangle";
minX: number;
maxX: number;
minY: number;
maxY: number;
}
export interface singleFaceGroup_faceGroup_imageFaces_media_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface singleFaceGroup_faceGroup_imageFaces_media_highRes {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface singleFaceGroup_faceGroup_imageFaces_media {
__typename: 'Media'
id: string
type: MediaType
title: string
__typename: "Media";
id: string;
type: MediaType;
title: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: singleFaceGroup_faceGroup_imageFaces_media_thumbnail | null
thumbnail: singleFaceGroup_faceGroup_imageFaces_media_thumbnail | null;
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: singleFaceGroup_faceGroup_imageFaces_media_highRes | null
favorite: boolean
highRes: singleFaceGroup_faceGroup_imageFaces_media_highRes | null;
favorite: boolean;
}
export interface singleFaceGroup_faceGroup_imageFaces {
__typename: 'ImageFace'
id: string
rectangle: singleFaceGroup_faceGroup_imageFaces_rectangle
media: singleFaceGroup_faceGroup_imageFaces_media
__typename: "ImageFace";
id: string;
rectangle: singleFaceGroup_faceGroup_imageFaces_rectangle;
media: singleFaceGroup_faceGroup_imageFaces_media;
}
export interface singleFaceGroup_faceGroup {
__typename: 'FaceGroup'
id: string
label: string | null
imageFaces: singleFaceGroup_faceGroup_imageFaces[]
__typename: "FaceGroup";
id: string;
label: string | null;
imageFaces: singleFaceGroup_faceGroup_imageFaces[];
}
export interface singleFaceGroup {
faceGroup: singleFaceGroup_faceGroup
faceGroup: singleFaceGroup_faceGroup;
}
export interface singleFaceGroupVariables {
id: string
limit: number
offset: number
id: string;
limit: number;
offset: number;
}

View File

@ -8,59 +8,59 @@
// ====================================================
export interface myFaces_myFaceGroups_imageFaces_rectangle {
__typename: 'FaceRectangle'
minX: number
maxX: number
minY: number
maxY: number
__typename: "FaceRectangle";
minX: number;
maxX: number;
minY: number;
maxY: number;
}
export interface myFaces_myFaceGroups_imageFaces_media_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface myFaces_myFaceGroups_imageFaces_media {
__typename: 'Media'
id: string
title: string
__typename: "Media";
id: string;
title: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: myFaces_myFaceGroups_imageFaces_media_thumbnail | null
thumbnail: myFaces_myFaceGroups_imageFaces_media_thumbnail | null;
}
export interface myFaces_myFaceGroups_imageFaces {
__typename: 'ImageFace'
id: string
rectangle: myFaces_myFaceGroups_imageFaces_rectangle
media: myFaces_myFaceGroups_imageFaces_media
__typename: "ImageFace";
id: string;
rectangle: myFaces_myFaceGroups_imageFaces_rectangle;
media: myFaces_myFaceGroups_imageFaces_media;
}
export interface myFaces_myFaceGroups {
__typename: 'FaceGroup'
id: string
label: string | null
imageFaceCount: number
imageFaces: myFaces_myFaceGroups_imageFaces[]
__typename: "FaceGroup";
id: string;
label: string | null;
imageFaceCount: number;
imageFaces: myFaces_myFaceGroups_imageFaces[];
}
export interface myFaces {
myFaceGroups: myFaces_myFaceGroups[]
myFaceGroups: myFaces_myFaceGroups[];
}
export interface myFacesVariables {
limit?: number | null
offset?: number | null
limit?: number | null;
offset?: number | null;
}

View File

@ -3,86 +3,86 @@
// @generated
// This file was automatically generated and should not be edited.
import { MediaType } from './../../../__generated__/globalTypes'
import { MediaType } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL query operation: placePageQueryMedia
// ====================================================
export interface placePageQueryMedia_mediaList_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface placePageQueryMedia_mediaList_highRes {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface placePageQueryMedia_mediaList_videoWeb {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface placePageQueryMedia_mediaList {
__typename: 'Media'
id: string
title: string
__typename: "Media";
id: string;
title: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: placePageQueryMedia_mediaList_thumbnail | null
thumbnail: placePageQueryMedia_mediaList_thumbnail | null;
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: placePageQueryMedia_mediaList_highRes | null
highRes: placePageQueryMedia_mediaList_highRes | null;
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: placePageQueryMedia_mediaList_videoWeb | null
type: MediaType
videoWeb: placePageQueryMedia_mediaList_videoWeb | null;
type: MediaType;
}
export interface placePageQueryMedia {
/**
* Get a list of media by their ids, user must own the media or be admin
*/
mediaList: placePageQueryMedia_mediaList[]
mediaList: placePageQueryMedia_mediaList[];
}
export interface placePageQueryMediaVariables {
mediaIDs: string[]
mediaIDs: string[];
}

View File

@ -8,15 +8,15 @@
// ====================================================
export interface changeUserPassword_updateUser {
__typename: 'User'
id: string
__typename: "User";
id: string;
}
export interface changeUserPassword {
updateUser: changeUserPassword_updateUser
updateUser: changeUserPassword_updateUser;
}
export interface changeUserPasswordVariables {
userId: string
password: string
userId: string;
password: string;
}

View File

@ -8,17 +8,17 @@
// ====================================================
export interface createUser_createUser {
__typename: 'User'
id: string
username: string
admin: boolean
__typename: "User";
id: string;
username: string;
admin: boolean;
}
export interface createUser {
createUser: createUser_createUser
createUser: createUser_createUser;
}
export interface createUserVariables {
username: string
admin: boolean
username: string;
admin: boolean;
}

View File

@ -8,15 +8,15 @@
// ====================================================
export interface deleteUser_deleteUser {
__typename: 'User'
id: string
username: string
__typename: "User";
id: string;
username: string;
}
export interface deleteUser {
deleteUser: deleteUser_deleteUser
deleteUser: deleteUser_deleteUser;
}
export interface deleteUserVariables {
id: string
id: string;
}

View File

@ -8,18 +8,18 @@
// ====================================================
export interface updateUser_updateUser {
__typename: 'User'
id: string
username: string
admin: boolean
__typename: "User";
id: string;
username: string;
admin: boolean;
}
export interface updateUser {
updateUser: updateUser_updateUser
updateUser: updateUser_updateUser;
}
export interface updateUserVariables {
id: string
username?: string | null
admin?: boolean | null
id: string;
username?: string | null;
admin?: boolean | null;
}

View File

@ -3,22 +3,22 @@
// @generated
// This file was automatically generated and should not be edited.
import { LanguageTranslation } from './../../../__generated__/globalTypes'
import { LanguageTranslation } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL mutation operation: changeUserPreferences
// ====================================================
export interface changeUserPreferences_changeUserPreferences {
__typename: 'UserPreferences'
id: string
language: LanguageTranslation | null
__typename: "UserPreferences";
id: string;
language: LanguageTranslation | null;
}
export interface changeUserPreferences {
changeUserPreferences: changeUserPreferences_changeUserPreferences
changeUserPreferences: changeUserPreferences_changeUserPreferences;
}
export interface changeUserPreferencesVariables {
language?: string | null
language?: string | null;
}

View File

@ -3,18 +3,18 @@
// @generated
// This file was automatically generated and should not be edited.
import { LanguageTranslation } from './../../../__generated__/globalTypes'
import { LanguageTranslation } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL query operation: myUserPreferences
// ====================================================
export interface myUserPreferences_myUserPreferences {
__typename: 'UserPreferences'
id: string
language: LanguageTranslation | null
__typename: "UserPreferences";
id: string;
language: LanguageTranslation | null;
}
export interface myUserPreferences {
myUserPreferences: myUserPreferences_myUserPreferences
myUserPreferences: myUserPreferences_myUserPreferences;
}

View File

@ -3,172 +3,172 @@
// @generated
// This file was automatically generated and should not be edited.
import { MediaType } from './../../../__generated__/globalTypes'
import { MediaType } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL query operation: SharePageToken
// ====================================================
export interface SharePageToken_shareToken_album {
__typename: 'Album'
id: string
__typename: "Album";
id: string;
}
export interface SharePageToken_shareToken_media_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface SharePageToken_shareToken_media_downloads_mediaUrl {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
/**
* The file size of the resource in bytes
*/
fileSize: number
fileSize: number;
}
export interface SharePageToken_shareToken_media_downloads {
__typename: 'MediaDownload'
title: string
mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl
__typename: "MediaDownload";
title: string;
mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl;
}
export interface SharePageToken_shareToken_media_highRes {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface SharePageToken_shareToken_media_videoWeb {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface SharePageToken_shareToken_media_exif {
__typename: 'MediaEXIF'
id: string
__typename: "MediaEXIF";
id: string;
/**
* The model name of the camera
*/
camera: string | null
camera: string | null;
/**
* The maker of the camera
*/
maker: string | null
maker: string | null;
/**
* The name of the lens
*/
lens: string | null
dateShot: any | null
lens: string | null;
dateShot: any | null;
/**
* The exposure time of the image
*/
exposure: number | null
exposure: number | null;
/**
* The aperature stops of the image
*/
aperture: number | null
aperture: number | null;
/**
* The ISO setting of the image
*/
iso: number | null
iso: number | null;
/**
* The focal length of the lens, when the image was taken
*/
focalLength: number | null
focalLength: number | null;
/**
* A formatted description of the flash settings, when the image was taken
*/
flash: number | null
flash: number | null;
/**
* An index describing the mode for adjusting the exposure of the image
*/
exposureProgram: number | null
exposureProgram: number | null;
}
export interface SharePageToken_shareToken_media {
__typename: 'Media'
id: string
title: string
type: MediaType
__typename: "Media";
id: string;
title: string;
type: MediaType;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: SharePageToken_shareToken_media_thumbnail | null
downloads: SharePageToken_shareToken_media_downloads[]
thumbnail: SharePageToken_shareToken_media_thumbnail | null;
downloads: SharePageToken_shareToken_media_downloads[];
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: SharePageToken_shareToken_media_highRes | null
highRes: SharePageToken_shareToken_media_highRes | null;
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: SharePageToken_shareToken_media_videoWeb | null
exif: SharePageToken_shareToken_media_exif | null
videoWeb: SharePageToken_shareToken_media_videoWeb | null;
exif: SharePageToken_shareToken_media_exif | null;
}
export interface SharePageToken_shareToken {
__typename: 'ShareToken'
token: string
__typename: "ShareToken";
token: string;
/**
* The album this token shares
*/
album: SharePageToken_shareToken_album | null
album: SharePageToken_shareToken_album | null;
/**
* The media this token shares
*/
media: SharePageToken_shareToken_media | null
media: SharePageToken_shareToken_media | null;
}
export interface SharePageToken {
shareToken: SharePageToken_shareToken
shareToken: SharePageToken_shareToken;
}
export interface SharePageTokenVariables {
token: string
password?: string | null
token: string;
password?: string | null;
}

View File

@ -3,179 +3,179 @@
// @generated
// This file was automatically generated and should not be edited.
import { OrderDirection, MediaType } from './../../../__generated__/globalTypes'
import { OrderDirection, MediaType } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL query operation: shareAlbumQuery
// ====================================================
export interface shareAlbumQuery_album_subAlbums_thumbnail_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface shareAlbumQuery_album_subAlbums_thumbnail {
__typename: 'Media'
id: string
__typename: "Media";
id: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null
thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null;
}
export interface shareAlbumQuery_album_subAlbums {
__typename: 'Album'
id: string
title: string
__typename: "Album";
id: string;
title: string;
/**
* An image in this album used for previewing this album
*/
thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null
thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null;
}
export interface shareAlbumQuery_album_media_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface shareAlbumQuery_album_media_downloads_mediaUrl {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
/**
* The file size of the resource in bytes
*/
fileSize: number
fileSize: number;
}
export interface shareAlbumQuery_album_media_downloads {
__typename: 'MediaDownload'
title: string
mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl
__typename: "MediaDownload";
title: string;
mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl;
}
export interface shareAlbumQuery_album_media_highRes {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface shareAlbumQuery_album_media_videoWeb {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface shareAlbumQuery_album_media_exif {
__typename: 'MediaEXIF'
__typename: "MediaEXIF";
/**
* The model name of the camera
*/
camera: string | null
camera: string | null;
/**
* The maker of the camera
*/
maker: string | null
maker: string | null;
/**
* The name of the lens
*/
lens: string | null
dateShot: any | null
lens: string | null;
dateShot: any | null;
/**
* The exposure time of the image
*/
exposure: number | null
exposure: number | null;
/**
* The aperature stops of the image
*/
aperture: number | null
aperture: number | null;
/**
* The ISO setting of the image
*/
iso: number | null
iso: number | null;
/**
* The focal length of the lens, when the image was taken
*/
focalLength: number | null
focalLength: number | null;
/**
* A formatted description of the flash settings, when the image was taken
*/
flash: number | null
flash: number | null;
/**
* An index describing the mode for adjusting the exposure of the image
*/
exposureProgram: number | null
exposureProgram: number | null;
}
export interface shareAlbumQuery_album_media {
__typename: 'Media'
id: string
title: string
type: MediaType
__typename: "Media";
id: string;
title: string;
type: MediaType;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: shareAlbumQuery_album_media_thumbnail | null
downloads: shareAlbumQuery_album_media_downloads[]
thumbnail: shareAlbumQuery_album_media_thumbnail | null;
downloads: shareAlbumQuery_album_media_downloads[];
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: shareAlbumQuery_album_media_highRes | null
highRes: shareAlbumQuery_album_media_highRes | null;
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: shareAlbumQuery_album_media_videoWeb | null
exif: shareAlbumQuery_album_media_exif | null
videoWeb: shareAlbumQuery_album_media_videoWeb | null;
exif: shareAlbumQuery_album_media_exif | null;
}
export interface shareAlbumQuery_album {
__typename: 'Album'
id: string
title: string
__typename: "Album";
id: string;
title: string;
/**
* The albums contained in this album
*/
subAlbums: shareAlbumQuery_album_subAlbums[]
subAlbums: shareAlbumQuery_album_subAlbums[];
/**
* The media inside this album
*/
media: shareAlbumQuery_album_media[]
media: shareAlbumQuery_album_media[];
}
export interface shareAlbumQuery {
@ -183,15 +183,15 @@ export interface shareAlbumQuery {
* Get album by id, user must own the album or be admin
* If valid tokenCredentials are provided, the album may be retrived without further authentication
*/
album: shareAlbumQuery_album
album: shareAlbumQuery_album;
}
export interface shareAlbumQueryVariables {
id: string
token: string
password?: string | null
mediaOrderBy?: string | null
mediaOrderDirection?: OrderDirection | null
limit?: number | null
offset?: number | null
id: string;
token: string;
password?: string | null;
mediaOrderBy?: string | null;
mediaOrderDirection?: OrderDirection | null;
limit?: number | null;
offset?: number | null;
}

View File

@ -1,18 +1,18 @@
import React from 'react'
import Layout from '../../components/layout/Layout'
import TimelineGallery from '../../components/timelineGallery/TimelineGallery'
import { useTranslation } from 'react-i18next'
import TimelineGallery from '../../components/timelineGallery/TimelineGallery'
const PhotosPage = () => {
const TimelinePage = () => {
const { t } = useTranslation()
return (
<>
<Layout title={t('photos_page.title', 'Photos')}>
<Layout title={t('photos_page.title', 'Timeline')}>
<TimelineGallery />
</Layout>
</>
)
}
export default PhotosPage
export default TimelinePage

View File

@ -8,34 +8,34 @@
//==============================================================
export enum LanguageTranslation {
Danish = 'Danish',
English = 'English',
French = 'French',
German = 'German',
Italian = 'Italian',
Polish = 'Polish',
Portuguese = 'Portuguese',
Russian = 'Russian',
SimplifiedChinese = 'SimplifiedChinese',
Spanish = 'Spanish',
Swedish = 'Swedish',
TraditionalChinese = 'TraditionalChinese',
Danish = "Danish",
English = "English",
French = "French",
German = "German",
Italian = "Italian",
Polish = "Polish",
Portuguese = "Portuguese",
Russian = "Russian",
SimplifiedChinese = "SimplifiedChinese",
Spanish = "Spanish",
Swedish = "Swedish",
TraditionalChinese = "TraditionalChinese",
}
export enum MediaType {
Photo = 'Photo',
Video = 'Video',
Photo = "Photo",
Video = "Video",
}
export enum NotificationType {
Close = 'Close',
Message = 'Message',
Progress = 'Progress',
Close = "Close",
Message = "Message",
Progress = "Progress",
}
export enum OrderDirection {
ASC = 'ASC',
DESC = 'DESC',
ASC = "ASC",
DESC = "DESC",
}
//==============================================================

View File

@ -3,18 +3,18 @@
// @generated
// This file was automatically generated and should not be edited.
import { LanguageTranslation } from './globalTypes'
import { LanguageTranslation } from "./globalTypes";
// ====================================================
// GraphQL query operation: siteTranslation
// ====================================================
export interface siteTranslation_myUserPreferences {
__typename: 'UserPreferences'
id: string
language: LanguageTranslation | null
__typename: "UserPreferences";
id: string;
language: LanguageTranslation | null;
}
export interface siteTranslation {
myUserPreferences: siteTranslation_myUserPreferences
myUserPreferences: siteTranslation_myUserPreferences;
}

View File

@ -10,7 +10,7 @@ import { ReactComponent as DirectionIcon } from './icons/direction-arrow.svg'
import Dropdown from '../../primitives/form/Dropdown'
type FavoriteCheckboxProps = {
export type FavoriteCheckboxProps = {
onlyFavorites: boolean
setOnlyFavorites(favorites: boolean): void
}
@ -79,7 +79,7 @@ const SortingOptions = ({ setOrdering, ordering }: SortingOptionsProps) => {
<fieldset>
<legend id="filter_group_sort-label" className="inline-block mb-1">
<SortingIcon
className="inline-block align-baseline mr-1"
className="inline-block align-baseline mr-1 mt-1"
aria-hidden="true"
/>
<span>{t('album_filter.sort', 'Sort')}</span>

View File

@ -8,15 +8,15 @@
// ====================================================
export interface albumPathQuery_album_path {
__typename: 'Album'
id: string
title: string
__typename: "Album";
id: string;
title: string;
}
export interface albumPathQuery_album {
__typename: 'Album'
id: string
path: albumPathQuery_album_path[]
__typename: "Album";
id: string;
path: albumPathQuery_album_path[];
}
export interface albumPathQuery {
@ -24,9 +24,9 @@ export interface albumPathQuery {
* Get album by id, user must own the album or be admin
* If valid tokenCredentials are provided, the album may be retrived without further authentication
*/
album: albumPathQuery_album
album: albumPathQuery_album;
}
export interface albumPathQueryVariables {
id: string
id: string;
}

View File

@ -80,7 +80,7 @@ export const MainMenu = () => {
<div className="fixed w-full bottom-0 lg:bottom-auto lg:top-[84px] z-30 bg-white shadow-separator lg:shadow-none lg:w-[240px] lg:ml-8 lg:mr-5 flex-shrink-0">
<ul className="flex justify-around py-2 px-2 max-w-lg mx-auto lg:flex-col lg:p-0">
<MenuButton
to="/photos"
to="/timeline"
exact
label={t('sidemenu.photos', 'Timeline')}
background="#8ac5f4"

View File

@ -8,13 +8,13 @@
// ====================================================
export interface adminQuery_myUser {
__typename: 'User'
admin: boolean
__typename: "User";
admin: boolean;
}
export interface adminQuery {
/**
* Information about the currently logged in user
*/
myUser: adminQuery_myUser
myUser: adminQuery_myUser;
}

View File

@ -8,13 +8,13 @@
// ====================================================
export interface faceDetectionEnabled_siteInfo {
__typename: 'SiteInfo'
__typename: "SiteInfo";
/**
* Whether or not face detection is enabled and working
*/
faceDetectionEnabled: boolean
faceDetectionEnabled: boolean;
}
export interface faceDetectionEnabled {
siteInfo: faceDetectionEnabled_siteInfo
siteInfo: faceDetectionEnabled_siteInfo;
}

View File

@ -11,5 +11,5 @@ export interface mapboxEnabledQuery {
/**
* Get the mapbox api token, returns null if mapbox is not enabled
*/
mapboxToken: string | null
mapboxToken: string | null;
}

View File

@ -3,27 +3,27 @@
// @generated
// This file was automatically generated and should not be edited.
import { NotificationType } from './../../../__generated__/globalTypes'
import { NotificationType } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL subscription operation: notificationSubscription
// ====================================================
export interface notificationSubscription_notification {
__typename: 'Notification'
key: string
type: NotificationType
header: string
content: string
progress: number | null
positive: boolean
negative: boolean
__typename: "Notification";
key: string;
type: NotificationType;
header: string;
content: string;
progress: number | null;
positive: boolean;
negative: boolean;
/**
* Time in milliseconds before the notification will close
*/
timeout: number | null
timeout: number | null;
}
export interface notificationSubscription {
notification: notificationSubscription_notification
notification: notificationSubscription_notification;
}

View File

@ -30,7 +30,7 @@ const Gallery = styled.div`
}
`
const PhotoFiller = styled.div`
export const PhotoFiller = styled.div`
height: 200px;
flex-grow: 999999;
`

View File

@ -8,19 +8,19 @@
// ====================================================
export interface markMediaFavorite_favoriteMedia {
__typename: 'Media'
id: string
favorite: boolean
__typename: "Media";
id: string;
favorite: boolean;
}
export interface markMediaFavorite {
/**
* Mark or unmark a media as being a favorite
*/
favoriteMedia: markMediaFavorite_favoriteMedia
favoriteMedia: markMediaFavorite_favoriteMedia;
}
export interface markMediaFavoriteVariables {
mediaId: string
favorite: boolean
mediaId: string;
favorite: boolean;
}

View File

@ -12,7 +12,9 @@ const AlbumsPage = React.lazy(
() => import('../../Pages/AllAlbumsPage/AlbumsPage')
)
const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage'))
const PhotosPage = React.lazy(() => import('../../Pages/PhotosPage/PhotosPage'))
const TimelinePage = React.lazy(
() => import('../../Pages/TimelinePage/TimelinePage')
)
const PlacesPage = React.lazy(() => import('../../Pages/PlacesPage/PlacesPage'))
const SharePage = React.lazy(() => import('../../Pages/SharePage/SharePage'))
const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage'))
@ -49,11 +51,17 @@ const Routes = () => {
<Route path="/share" component={SharePage} />
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
<AuthorizedRoute path="/photos" component={PhotosPage} />
<AuthorizedRoute path="/timeline" component={TimelinePage} />
<AuthorizedRoute path="/places" component={PlacesPage} />
<AuthorizedRoute path="/people/:person?" component={PeoplePage} />
<AuthorizedRoute path="/settings" component={SettingsPage} />
<Route path="/" exact render={() => <Redirect to="/photos" />} />
<Route path="/" exact render={() => <Redirect to="/timeline" />} />
{/* For backwards compatibility */}
<Route
path="/photos"
exact
render={() => <Redirect to="/timeline" />}
/>
<Route
render={() => (
<div>{t('routes.page_not_found', 'Page not found')}</div>

View File

@ -8,38 +8,38 @@
// ====================================================
export interface resetAlbumCover_resetAlbumCover_thumbnail_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface resetAlbumCover_resetAlbumCover_thumbnail {
__typename: 'Media'
id: string
__typename: "Media";
id: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail_thumbnail | null
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail_thumbnail | null;
}
export interface resetAlbumCover_resetAlbumCover {
__typename: 'Album'
id: string
__typename: "Album";
id: string;
/**
* An image in this album used for previewing this album
*/
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail | null
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail | null;
}
export interface resetAlbumCover {
/**
* Reset the assigned cover photo for an album
*/
resetAlbumCover: resetAlbumCover_resetAlbumCover
resetAlbumCover: resetAlbumCover_resetAlbumCover;
}
export interface resetAlbumCoverVariables {
albumID: string
albumID: string;
}

View File

@ -8,38 +8,38 @@
// ====================================================
export interface setAlbumCover_setAlbumCover_thumbnail_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
}
export interface setAlbumCover_setAlbumCover_thumbnail {
__typename: 'Media'
id: string
__typename: "Media";
id: string;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: setAlbumCover_setAlbumCover_thumbnail_thumbnail | null
thumbnail: setAlbumCover_setAlbumCover_thumbnail_thumbnail | null;
}
export interface setAlbumCover_setAlbumCover {
__typename: 'Album'
id: string
__typename: "Album";
id: string;
/**
* An image in this album used for previewing this album
*/
thumbnail: setAlbumCover_setAlbumCover_thumbnail | null
thumbnail: setAlbumCover_setAlbumCover_thumbnail | null;
}
export interface setAlbumCover {
/**
* Assign a cover photo to an album
*/
setAlbumCover: setAlbumCover_setAlbumCover
setAlbumCover: setAlbumCover_setAlbumCover;
}
export interface setAlbumCoverVariables {
coverID: string
coverID: string;
}

View File

@ -8,19 +8,19 @@
// ====================================================
export interface sidebarAlbumAddShare_shareAlbum {
__typename: 'ShareToken'
token: string
__typename: "ShareToken";
token: string;
}
export interface sidebarAlbumAddShare {
/**
* Generate share token for album
*/
shareAlbum: sidebarAlbumAddShare_shareAlbum
shareAlbum: sidebarAlbumAddShare_shareAlbum;
}
export interface sidebarAlbumAddShareVariables {
id: string
password?: string | null
expire?: any | null
id: string;
password?: string | null;
expire?: any | null;
}

View File

@ -8,19 +8,19 @@
// ====================================================
export interface sidebarGetAlbumShares_album_shares {
__typename: 'ShareToken'
id: string
token: string
__typename: "ShareToken";
id: string;
token: string;
/**
* Whether or not a password is needed to access the share
*/
hasPassword: boolean
hasPassword: boolean;
}
export interface sidebarGetAlbumShares_album {
__typename: 'Album'
id: string
shares: sidebarGetAlbumShares_album_shares[]
__typename: "Album";
id: string;
shares: sidebarGetAlbumShares_album_shares[];
}
export interface sidebarGetAlbumShares {
@ -28,9 +28,9 @@ export interface sidebarGetAlbumShares {
* Get album by id, user must own the album or be admin
* If valid tokenCredentials are provided, the album may be retrived without further authentication
*/
album: sidebarGetAlbumShares_album
album: sidebarGetAlbumShares_album;
}
export interface sidebarGetAlbumSharesVariables {
id: string
id: string;
}

View File

@ -8,19 +8,19 @@
// ====================================================
export interface sidebarGetPhotoShares_media_shares {
__typename: 'ShareToken'
id: string
token: string
__typename: "ShareToken";
id: string;
token: string;
/**
* Whether or not a password is needed to access the share
*/
hasPassword: boolean
hasPassword: boolean;
}
export interface sidebarGetPhotoShares_media {
__typename: 'Media'
id: string
shares: sidebarGetPhotoShares_media_shares[]
__typename: "Media";
id: string;
shares: sidebarGetPhotoShares_media_shares[];
}
export interface sidebarGetPhotoShares {
@ -28,9 +28,9 @@ export interface sidebarGetPhotoShares {
* Get media by id, user must own the media or be admin.
* If valid tokenCredentials are provided, the media may be retrived without further authentication
*/
media: sidebarGetPhotoShares_media
media: sidebarGetPhotoShares_media;
}
export interface sidebarGetPhotoSharesVariables {
id: string
id: string;
}

View File

@ -3,155 +3,155 @@
// @generated
// This file was automatically generated and should not be edited.
import { MediaType } from './../../../__generated__/globalTypes'
import { MediaType } from "./../../../__generated__/globalTypes";
// ====================================================
// GraphQL query operation: sidebarPhoto
// ====================================================
export interface sidebarPhoto_media_highRes {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface sidebarPhoto_media_thumbnail {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface sidebarPhoto_media_videoWeb {
__typename: 'MediaURL'
__typename: "MediaURL";
/**
* URL for previewing the image
*/
url: string
url: string;
/**
* Width of the image in pixels
*/
width: number
width: number;
/**
* Height of the image in pixels
*/
height: number
height: number;
}
export interface sidebarPhoto_media_videoMetadata {
__typename: 'VideoMetadata'
id: string
width: number
height: number
duration: number
codec: string | null
framerate: number | null
bitrate: string | null
colorProfile: string | null
audio: string | null
__typename: "VideoMetadata";
id: string;
width: number;
height: number;
duration: number;
codec: string | null;
framerate: number | null;
bitrate: string | null;
colorProfile: string | null;
audio: string | null;
}
export interface sidebarPhoto_media_exif {
__typename: 'MediaEXIF'
id: string
__typename: "MediaEXIF";
id: string;
/**
* The model name of the camera
*/
camera: string | null
camera: string | null;
/**
* The maker of the camera
*/
maker: string | null
maker: string | null;
/**
* The name of the lens
*/
lens: string | null
dateShot: any | null
lens: string | null;
dateShot: any | null;
/**
* The exposure time of the image
*/
exposure: number | null
exposure: number | null;
/**
* The aperature stops of the image
*/
aperture: number | null
aperture: number | null;
/**
* The ISO setting of the image
*/
iso: number | null
iso: number | null;
/**
* The focal length of the lens, when the image was taken
*/
focalLength: number | null
focalLength: number | null;
/**
* A formatted description of the flash settings, when the image was taken
*/
flash: number | null
flash: number | null;
/**
* An index describing the mode for adjusting the exposure of the image
*/
exposureProgram: number | null
exposureProgram: number | null;
}
export interface sidebarPhoto_media_faces_rectangle {
__typename: 'FaceRectangle'
minX: number
maxX: number
minY: number
maxY: number
__typename: "FaceRectangle";
minX: number;
maxX: number;
minY: number;
maxY: number;
}
export interface sidebarPhoto_media_faces_faceGroup {
__typename: 'FaceGroup'
id: string
__typename: "FaceGroup";
id: string;
}
export interface sidebarPhoto_media_faces {
__typename: 'ImageFace'
id: string
rectangle: sidebarPhoto_media_faces_rectangle
faceGroup: sidebarPhoto_media_faces_faceGroup
__typename: "ImageFace";
id: string;
rectangle: sidebarPhoto_media_faces_rectangle;
faceGroup: sidebarPhoto_media_faces_faceGroup;
}
export interface sidebarPhoto_media {
__typename: 'Media'
id: string
title: string
type: MediaType
__typename: "Media";
id: string;
title: string;
type: MediaType;
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: sidebarPhoto_media_highRes | null
highRes: sidebarPhoto_media_highRes | null;
/**
* URL to display the media in a smaller resolution
*/
thumbnail: sidebarPhoto_media_thumbnail | null
thumbnail: sidebarPhoto_media_thumbnail | null;
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: sidebarPhoto_media_videoWeb | null
videoMetadata: sidebarPhoto_media_videoMetadata | null
exif: sidebarPhoto_media_exif | null
faces: sidebarPhoto_media_faces[]
videoWeb: sidebarPhoto_media_videoWeb | null;
videoMetadata: sidebarPhoto_media_videoMetadata | null;
exif: sidebarPhoto_media_exif | null;
faces: sidebarPhoto_media_faces[];
}
export interface sidebarPhoto {
@ -159,9 +159,9 @@ export interface sidebarPhoto {
* Get media by id, user must own the media or be admin.
* If valid tokenCredentials are provided, the media may be retrived without further authentication
*/
media: sidebarPhoto_media
media: sidebarPhoto_media;
}
export interface sidebarPhotoVariables {
id: string
id: string;
}

View File

@ -8,19 +8,19 @@
// ====================================================
export interface sidebarPhotoAddShare_shareMedia {
__typename: 'ShareToken'
token: string
__typename: "ShareToken";
token: string;
}
export interface sidebarPhotoAddShare {
/**
* Generate share token for media
*/
shareMedia: sidebarPhotoAddShare_shareMedia
shareMedia: sidebarPhotoAddShare_shareMedia;
}
export interface sidebarPhotoAddShareVariables {
id: string
password?: string | null
expire?: any | null
id: string;
password?: string | null;
expire?: any | null;
}

View File

@ -8,22 +8,22 @@
// ====================================================
export interface sidebarProtectShare_protectShareToken {
__typename: 'ShareToken'
token: string
__typename: "ShareToken";
token: string;
/**
* Whether or not a password is needed to access the share
*/
hasPassword: boolean
hasPassword: boolean;
}
export interface sidebarProtectShare {
/**
* Set a password for a token, if null is passed for the password argument, the password will be cleared
*/
protectShareToken: sidebarProtectShare_protectShareToken
protectShareToken: sidebarProtectShare_protectShareToken;
}
export interface sidebarProtectShareVariables {
token: string
password?: string | null
token: string;
password?: string | null;
}

View File

@ -8,17 +8,17 @@
// ====================================================
export interface sidebareDeleteShare_deleteShareToken {
__typename: 'ShareToken'
token: string
__typename: "ShareToken";
token: string;
}
export interface sidebareDeleteShare {
/**
* Delete a share token by it's token value
*/
deleteShareToken: sidebareDeleteShare_deleteShareToken
deleteShareToken: sidebareDeleteShare_deleteShareToken;
}
export interface sidebareDeleteShareVariables {
token: string
token: string;
}

View File

@ -0,0 +1,103 @@
import { useQuery } from '@apollo/client'
import gql from 'graphql-tag'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown'
import { FavoriteCheckboxProps, FavoritesCheckbox } from '../album/AlbumFilter'
import { ReactComponent as DateIcon } from './icons/date.svg'
import { earliestMedia } from './__generated__/earliestMedia'
const EARLIEST_MEDIA_QUERY = gql`
query earliestMedia {
myMedia(
order: { order_by: "date_shot", order_direction: ASC }
paginate: { limit: 1 }
) {
id
date
}
}
`
type DateSelectorProps = {
filterDate: string | null
setFilterDate(date: string | null): void
}
const DateSelector = ({ filterDate, setFilterDate }: DateSelectorProps) => {
const { t } = useTranslation()
const { data, loading } = useQuery<earliestMedia>(EARLIEST_MEDIA_QUERY)
let items: DropdownItem[] = [
{
value: 'all',
label: t('timeline_filter.date.dropdown_all', 'From today'),
},
]
if (data) {
const dateStr = data.myMedia[0].date
const date = new Date(dateStr)
const now = new Date()
const currentYear = now.getFullYear()
const earliestYear = date.getFullYear()
const years: number[] = []
for (let i = currentYear - 1; i >= earliestYear; i--) {
years.push(i)
}
const yearItems = years.map<DropdownItem>(x => ({
value: `${x}`,
label: `${x} and earlier`,
}))
items = [...items, ...yearItems]
}
return (
<fieldset>
<legend id="filter_group_date-label" className="inline-block mb-1">
<DateIcon
className="inline-block align-baseline mr-1"
aria-hidden="true"
/>
<span>{t('timeline_filter.date.label', 'Date')}</span>
</legend>
<div>
<Dropdown
aria-labelledby="filter_group_date-label"
setSelected={date =>
date == 'all' ? setFilterDate(null) : setFilterDate(date)
}
value={filterDate || 'all'}
items={items}
disabled={loading}
/>
</div>
</fieldset>
)
}
type TimelineFiltersProps = DateSelectorProps & FavoriteCheckboxProps
const TimelineFilters = ({
onlyFavorites,
setOnlyFavorites,
filterDate,
setFilterDate,
}: TimelineFiltersProps) => {
return (
<div className="flex items-end gap-4 flex-wrap mb-4">
<DateSelector filterDate={filterDate} setFilterDate={setFilterDate} />
<FavoritesCheckbox
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
/>
</div>
)
}
export default TimelineFilters

View File

@ -0,0 +1,39 @@
import { MockedProvider } from '@apollo/client/testing'
import { render, screen } from '@testing-library/react'
import React from 'react'
import { MemoryRouter } from 'react-router-dom'
import TimelineGallery, { MY_TIMELINE_QUERY } from './TimelineGallery'
import { timelineData } from './timelineTestData'
jest.mock('../../hooks/useScrollPagination')
test('timeline with media', async () => {
const graphqlMocks = [
{
request: {
query: MY_TIMELINE_QUERY,
variables: { onlyFavorites: false, offset: 0, limit: 200 },
},
result: {
data: {
myTimeline: timelineData,
},
},
},
]
render(
<MemoryRouter initialEntries={['/timeline']}>
<MockedProvider mocks={graphqlMocks}>
<TimelineGallery />
</MockedProvider>
</MemoryRouter>
)
expect(screen.queryByLabelText('Show only favorites')).toBeInTheDocument()
expect(await screen.findAllByRole('link')).toHaveLength(4)
expect(await screen.findAllByRole('img')).toHaveLength(5)
screen.debug()
})

View File

@ -4,7 +4,6 @@ import { useQuery, gql } from '@apollo/client'
import TimelineGroupDate from './TimelineGroupDate'
import PresentView from '../photoGallery/presentView/PresentView'
import useURLParameters from '../../hooks/useURLParameters'
import { FavoritesCheckbox } from '../album/AlbumFilter'
import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../PaginateLoader'
import { useTranslation } from 'react-i18next'
@ -20,37 +19,42 @@ import {
import MediaSidebar from '../sidebar/MediaSidebar'
import { SidebarContext } from '../sidebar/Sidebar'
import { urlPresentModeSetupHook } from '../photoGallery/photoGalleryReducer'
import TimelineFilters from './TimelineFilters'
import client from '../../apolloClient'
const MY_TIMELINE_QUERY = gql`
query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) {
export const MY_TIMELINE_QUERY = gql`
query myTimeline(
$onlyFavorites: Boolean
$limit: Int
$offset: Int
$fromDate: Time
) {
myTimeline(
onlyFavorites: $onlyFavorites
fromDate: $fromDate
paginate: { limit: $limit, offset: $offset }
) {
id
title
type
thumbnail {
url
width
height
}
highRes {
url
width
height
}
videoWeb {
url
}
favorite
album {
id
title
}
media {
id
title
type
thumbnail {
url
width
height
}
highRes {
url
width
height
}
videoWeb {
url
}
favorite
}
mediaTotal
date
}
}
@ -63,7 +67,13 @@ export type TimelineActiveIndex = {
export type TimelineGroup = {
date: string
groups: myTimeline_myTimeline[]
albums: TimelineGroupAlbum[]
}
export type TimelineGroupAlbum = {
id: string
title: string
media: myTimeline_myTimeline[]
}
const TimelineGallery = () => {
@ -74,7 +84,10 @@ const TimelineGallery = () => {
const onlyFavorites = getParam('favorites') == '1' ? true : false
const setOnlyFavorites = (favorites: boolean) =>
setParam('favorites', favorites ? '1' : '0')
setParam('favorites', favorites ? '1' : null)
const filterDate = getParam('date')
const setFilterDate = (x: string) => setParam('date', x)
const favoritesNeedsRefresh = useRef(false)
@ -94,8 +107,11 @@ const TimelineGallery = () => {
>(MY_TIMELINE_QUERY, {
variables: {
onlyFavorites,
fromDate: filterDate
? `${parseInt(filterDate) + 1}-01-01T00:00:00Z`
: undefined,
offset: 0,
limit: 50,
limit: 200,
},
})
@ -123,6 +139,20 @@ const TimelineGallery = () => {
}
}, [mediaState.activeIndex])
useEffect(() => {
;(async () => {
await client.resetStore()
await refetch({
onlyFavorites,
fromDate: filterDate
? `${parseInt(filterDate) + 1}-01-01T00:00:00Z`
: undefined,
offset: 0,
limit: 200,
})
})()
}, [filterDate])
urlPresentModeSetupHook({
dispatchMedia,
openPresentMode: event => {
@ -155,12 +185,12 @@ const TimelineGallery = () => {
return (
<div className="overflow-x-hidden">
<div className="mb-2">
<FavoritesCheckbox
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
/>
</div>
<TimelineFilters
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
filterDate={filterDate}
setFilterDate={setFilterDate}
/>
<div className="-mx-3 flex flex-wrap" ref={containerElem}>
{timelineGroups}
</div>

View File

@ -1,7 +1,7 @@
import React from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { MediaThumbnail } from '../photoGallery/MediaThumbnail'
import { PhotoFiller } from '../photoGallery/PhotoGallery'
import {
toggleFavoriteAction,
useMarkFavoriteMutation,
@ -13,20 +13,6 @@ import {
TimelineGalleryState,
} from './timelineGalleryReducer'
const TotalItemsBubble = styled(Link)`
position: absolute;
top: 24px;
right: 6px;
background-color: white;
border-radius: 50%;
padding: 8px 0;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3);
color: black;
width: 36px;
height: 36px;
text-align: center;
`
type TimelineGroupAlbumProps = {
dateIndex: number
albumIndex: number
@ -40,8 +26,11 @@ const TimelineGroupAlbum = ({
mediaState,
dispatchMedia,
}: TimelineGroupAlbumProps) => {
const { media, mediaTotal, album } =
mediaState.timelineGroups[dateIndex].groups[albumIndex]
const {
media,
title: albumTitle,
id: albumID,
} = mediaState.timelineGroups[dateIndex].albums[albumIndex]
const [markFavorite] = useMarkFavoriteMutation()
@ -79,24 +68,14 @@ const TimelineGroupAlbum = ({
/>
))
let itemsBubble = null
const mediaVisibleCount = media.length
if (mediaTotal > mediaVisibleCount) {
itemsBubble = (
<TotalItemsBubble to={`/album/${album.id}`}>
{`+${Math.min(mediaTotal - mediaVisibleCount, 99)}`}
</TotalItemsBubble>
)
}
return (
<div className="mx-2">
<Link to={`/album/${album.id}`} className="hover:underline">
{album.title}
<Link to={`/album/${albumID}`} className="hover:underline">
{albumTitle}
</Link>
<div className="flex flex-wrap items-center h-[210px] relative -mx-1 pr-4 overflow-hidden">
<div className="flex flex-wrap items-center relative -mx-1 overflow-hidden">
{mediaElms}
{itemsBubble}
<PhotoFiller />
</div>
</div>
)

View File

@ -27,9 +27,9 @@ const TimelineGroupDate = ({
const group = mediaState.timelineGroups[groupIndex]
const albumGroupElms = group.groups.map((group, i) => (
const albumGroupElms = group.albums.map((album, i) => (
<TimelineGroupAlbum
key={`${group.date}_${group.album.id}`}
key={`${group.date}_${album.id}`}
dateIndex={groupIndex}
albumIndex={i}
mediaState={mediaState}

View File

@ -0,0 +1,24 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: earliestMedia
// ====================================================
export interface earliestMedia_myMedia {
__typename: 'Media'
id: string
/**
* The date the image was shot or the date it was imported as a fallback
*/
date: any
}
export interface earliestMedia {
/**
* List of media owned by the logged in user
*/
myMedia: earliestMedia_myMedia[]
}

View File

@ -9,53 +9,53 @@ import { MediaType } from './../../../__generated__/globalTypes'
// GraphQL query operation: myTimeline
// ====================================================
export interface myTimeline_myTimeline_thumbnail {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
/**
* Width of the image in pixels
*/
width: number
/**
* Height of the image in pixels
*/
height: number
}
export interface myTimeline_myTimeline_highRes {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
/**
* Width of the image in pixels
*/
width: number
/**
* Height of the image in pixels
*/
height: number
}
export interface myTimeline_myTimeline_videoWeb {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface myTimeline_myTimeline_album {
__typename: 'Album'
id: string
title: string
}
export interface myTimeline_myTimeline_media_thumbnail {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
/**
* Width of the image in pixels
*/
width: number
/**
* Height of the image in pixels
*/
height: number
}
export interface myTimeline_myTimeline_media_highRes {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
/**
* Width of the image in pixels
*/
width: number
/**
* Height of the image in pixels
*/
height: number
}
export interface myTimeline_myTimeline_media_videoWeb {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface myTimeline_myTimeline_media {
export interface myTimeline_myTimeline {
__typename: 'Media'
id: string
title: string
@ -63,27 +63,30 @@ export interface myTimeline_myTimeline_media {
/**
* URL to display the media in a smaller resolution
*/
thumbnail: myTimeline_myTimeline_media_thumbnail | null
thumbnail: myTimeline_myTimeline_thumbnail | null
/**
* URL to display the photo in full resolution, will be null for videos
*/
highRes: myTimeline_myTimeline_media_highRes | null
highRes: myTimeline_myTimeline_highRes | null
/**
* URL to get the video in a web format that can be played in the browser, will be null for photos
*/
videoWeb: myTimeline_myTimeline_media_videoWeb | null
videoWeb: myTimeline_myTimeline_videoWeb | null
favorite: boolean
}
export interface myTimeline_myTimeline {
__typename: 'TimelineGroup'
/**
* The album that holds the media
*/
album: myTimeline_myTimeline_album
media: myTimeline_myTimeline_media[]
mediaTotal: number
/**
* The date the image was shot or the date it was imported as a fallback
*/
date: any
}
export interface myTimeline {
/**
* Get a list of media, ordered first by day, then by album if multiple media was found for the same day.
*/
myTimeline: myTimeline_myTimeline[]
}
@ -91,4 +94,5 @@ export interface myTimelineVariables {
onlyFavorites?: boolean | null
limit?: number | null
offset?: number | null
fromDate?: any | null
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="13px" height="15px" viewBox="0 0 13 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M9.16666667,5.68434189e-14 C9.41979718,5.68434189e-14 9.62899397,0.188102588 9.66210226,0.432152962 L9.66666667,0.5 L9.666,1.333 L11.1666667,1.33333333 C12.1285626,1.33333333 12.9174396,2.07411552 12.9939226,3.01630483 L13,3.16666667 L13,12.5 C13,13.512522 12.1791887,14.3333333 11.1666667,14.3333333 L11.1666667,14.3333333 L1.83333333,14.3333333 C0.820811292,14.3333333 0,13.512522 0,12.5 L0,12.5 L0,3.16666667 C0,2.15414463 0.820811292,1.33333333 1.83333333,1.33333333 L1.83333333,1.33333333 L3.333,1.333 L3.33333333,0.5 C3.33333333,0.223857625 3.55719096,5.68434189e-14 3.83333333,5.68434189e-14 C4.08646384,5.68434189e-14 4.29566064,0.188102588 4.32876892,0.432152962 L4.33333333,0.5 L4.333,1.333 L8.666,1.333 L8.66666667,0.5 C8.66666667,0.223857625 8.89052429,5.68434189e-14 9.16666667,5.68434189e-14 Z M12,6.333 L1,6.333 L1,12.5 C1,12.9248344 1.31790432,13.2754183 1.72880177,13.3268405 L1.83333333,13.3333333 L11.1666667,13.3333333 C11.626904,13.3333333 12,12.9602373 12,12.5 L12,12.5 L12,6.333 Z M3.333,2.333 L1.83333333,2.33333333 C1.37309604,2.33333333 1,2.70642938 1,3.16666667 L1,3.16666667 L1,5.333 L12,5.333 L12,3.16666667 C12,2.74183224 11.6820957,2.39124835 11.2711982,2.33982618 L11.1666667,2.33333333 L9.666,2.333 L9.66666667,3.16666667 C9.66666667,3.44280904 9.44280904,3.66666667 9.16666667,3.66666667 C8.91353616,3.66666667 8.70433936,3.47856408 8.67123108,3.2345137 L8.66666667,3.16666667 L8.666,2.333 L4.333,2.333 L4.33333333,3.16666667 C4.33333333,3.44280904 4.10947571,3.66666667 3.83333333,3.66666667 C3.58020282,3.66666667 3.37100603,3.47856408 3.33789774,3.2345137 L3.33333333,3.16666667 L3.333,2.333 Z" fill="currentColor" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,10 +1,10 @@
import { myTimeline_myTimeline } from './__generated__/myTimeline'
import { MediaType } from '../../__generated__/globalTypes'
import {
timelineGalleryReducer,
TimelineGalleryState,
TimelineMediaIndex,
} from './timelineGalleryReducer'
import { timelineData } from './timelineTestData'
describe('timeline gallery reducer', () => {
const defaultEmptyState: TimelineGalleryState = {
@ -17,125 +17,6 @@ describe('timeline gallery reducer', () => {
timelineGroups: [],
}
const timelineData: myTimeline_myTimeline[] = [
{
album: {
id: '5',
title: 'first album',
__typename: 'Album',
},
media: [
{
id: '165',
title: '3666760020.jpg',
type: MediaType.Photo,
thumbnail: {
url: 'http://localhost:4001/photo/thumbnail_3666760020_jpg_x76GG5pS.jpg',
width: 768,
height: 1024,
__typename: 'MediaURL',
},
highRes: {
url: 'http://localhost:4001/photo/3666760020_wijGDNZ2.jpg',
width: 3024,
height: 4032,
__typename: 'MediaURL',
},
videoWeb: null,
favorite: false,
__typename: 'Media',
},
{
id: '184',
title: '7414455077.jpg',
type: MediaType.Photo,
thumbnail: {
url: 'http://localhost:4001/photo/thumbnail_7414455077_jpg_9JYHHYh6.jpg',
width: 768,
height: 1024,
__typename: 'MediaURL',
},
highRes: {
url: 'http://localhost:4001/photo/7414455077_0ejDBiKr.jpg',
width: 3024,
height: 4032,
__typename: 'MediaURL',
},
videoWeb: null,
favorite: false,
__typename: 'Media',
},
],
mediaTotal: 5,
date: '2019-09-21T00:00:00Z',
__typename: 'TimelineGroup',
},
{
album: {
id: '5',
title: 'another album',
__typename: 'Album',
},
media: [
{
id: '165',
title: '3666760020.jpg',
type: MediaType.Photo,
thumbnail: {
url: 'http://localhost:4001/photo/thumbnail_3666760020_jpg_x76GG5pS.jpg',
width: 768,
height: 1024,
__typename: 'MediaURL',
},
highRes: {
url: 'http://localhost:4001/photo/3666760020_wijGDNZ2.jpg',
width: 3024,
height: 4032,
__typename: 'MediaURL',
},
videoWeb: null,
favorite: false,
__typename: 'Media',
},
],
mediaTotal: 7,
date: '2019-09-21T00:00:00Z',
__typename: 'TimelineGroup',
},
{
__typename: 'TimelineGroup',
album: {
__typename: 'Album',
id: '5',
title: 'album on another day',
},
date: '2019-09-13T00:00:00Z',
mediaTotal: 1,
media: [
{
__typename: 'Media',
favorite: false,
videoWeb: null,
thumbnail: {
url: 'http://localhost:4001/photo/thumbnail_3666760020_jpg_x76GG5pS.jpg',
width: 768,
height: 1024,
__typename: 'MediaURL',
},
highRes: {
url: 'http://localhost:4001/photo/3666760020_wijGDNZ2.jpg',
width: 3024,
height: 4032,
__typename: 'MediaURL',
},
id: '321',
title: 'asdfimg.jpg',
type: MediaType.Photo,
},
],
},
]
const defaultState = timelineGalleryReducer(defaultEmptyState, {
type: 'replaceTimelineGroups',
timeline: timelineData,
@ -151,68 +32,167 @@ describe('timeline gallery reducer', () => {
},
timelineGroups: [
{
date: '2019-09-21T00:00:00Z',
groups: [
date: '2020-12-13T00:00:00Z',
albums: [
{
album: {
id: '5',
title: 'first album',
},
date: '2019-09-21T00:00:00Z',
id: '522',
media: [
{
__typename: 'Media',
album: {
__typename: 'Album',
id: '522',
title: 'random',
},
date: '2020-12-13T18:03:40Z',
favorite: false,
highRes: {},
id: '165',
thumbnail: {},
title: '3666760020.jpg',
type: 'Photo',
},
{
highRes: {},
id: '184',
thumbnail: {},
title: '7414455077.jpg',
highRes: {
__typename: 'MediaURL',
height: 4480,
url: 'http://localhost:4001/photo/122A2876_5cSPMiKL.jpg',
width: 6720,
},
id: '1058',
thumbnail: {
__typename: 'MediaURL',
height: 682,
url: 'http://localhost:4001/photo/thumbnail_122A2876_jpg_Kp1U80vD.jpg',
width: 1024,
},
title: '122A2876.jpg',
type: 'Photo',
videoWeb: null,
},
],
mediaTotal: 5,
},
{
album: {
id: '5',
title: 'another album',
},
date: '2019-09-21T00:00:00Z',
media: [
{
id: '165',
},
],
mediaTotal: 7,
title: 'random',
},
],
},
{
date: '2019-09-13T00:00:00Z',
groups: [
date: '2020-11-25T00:00:00Z',
albums: [
{
album: {
id: '5',
title: 'album on another day',
},
date: '2019-09-13T00:00:00Z',
id: '523',
title: 'another_album',
media: [
{
__typename: 'Media',
album: {
__typename: 'Album',
id: '523',
title: 'another_album',
},
date: '2020-11-25T16:14:33Z',
favorite: false,
highRes: {},
id: '321',
thumbnail: {},
title: 'asdfimg.jpg',
highRes: {
__typename: 'MediaURL',
height: 4118,
url: 'http://localhost:4001/photo/122A2630-Edit_ySQWFAgE.jpg',
width: 6177,
},
id: '1059',
thumbnail: {
__typename: 'MediaURL',
height: 682,
url: 'http://localhost:4001/photo/thumbnail_122A2630-Edit_jpg_pwjtMkpy.jpg',
width: 1024,
},
title: '122A2630-Edit.jpg',
type: 'Photo',
videoWeb: null,
},
{
__typename: 'Media',
album: {
__typename: 'Album',
id: '523',
title: 'another_album',
},
date: '2020-11-25T16:43:59Z',
favorite: false,
highRes: {
__typename: 'MediaURL',
height: 884,
url: 'http://localhost:4001/photo/122A2785-2_mCnWjLdb.jpg',
width: 884,
},
id: '1060',
thumbnail: {
__typename: 'MediaURL',
height: 1024,
url: 'http://localhost:4001/photo/thumbnail_122A2785-2_jpg_CevmxEXf.jpg',
width: 1024,
},
title: '122A2785-2.jpg',
type: 'Photo',
videoWeb: null,
},
],
},
{
id: '522',
title: 'random',
media: [
{
__typename: 'Media',
album: {
__typename: 'Album',
id: '522',
title: 'random',
},
date: '2020-11-25T16:14:33Z',
favorite: false,
highRes: {
__typename: 'MediaURL',
height: 4118,
url: 'http://localhost:4001/photo/122A2630-Edit_em9g89qg.jpg',
width: 6177,
},
id: '1056',
thumbnail: {
__typename: 'MediaURL',
height: 682,
url: 'http://localhost:4001/photo/thumbnail_122A2630-Edit_jpg_aJPCSDDl.jpg',
width: 1024,
},
title: '122A2630-Edit.jpg',
type: 'Photo',
videoWeb: null,
},
],
},
],
},
{
date: '2020-11-09T00:00:00Z',
albums: [
{
id: '522',
title: 'random',
media: [
{
__typename: 'Media',
id: '1054',
title: '122A2559.jpg',
type: MediaType.Photo,
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_122A2559_jpg_MsOJtPi8.jpg',
width: 1024,
height: 712,
},
highRes: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/122A2559_FDsQHuBN.jpg',
width: 6246,
height: 4346,
},
videoWeb: null,
favorite: false,
album: { __typename: 'Album', id: '522', title: 'random' },
date: '2020-11-09T15:38:09Z',
},
],
mediaTotal: 1,
},
],
},
@ -267,20 +247,20 @@ describe('timeline gallery reducer', () => {
media: 0,
},
out: {
date: 0,
date: 1,
album: 0,
media: 1,
media: 0,
},
},
{
name: 'next album',
in: {
date: 0,
date: 1,
album: 0,
media: 1,
},
out: {
date: 0,
date: 1,
album: 1,
media: 0,
},
@ -288,12 +268,12 @@ describe('timeline gallery reducer', () => {
{
name: 'next date',
in: {
date: 0,
date: 1,
album: 1,
media: 1,
media: 0,
},
out: {
date: 1,
date: 2,
album: 0,
media: 0,
},
@ -301,12 +281,12 @@ describe('timeline gallery reducer', () => {
{
name: 'reached end',
in: {
date: 1,
date: 2,
album: 0,
media: 0,
},
out: {
date: 1,
date: 2,
album: 0,
media: 0,
},
@ -366,12 +346,12 @@ describe('timeline gallery reducer', () => {
{
name: 'previous album',
in: {
date: 0,
date: 1,
album: 1,
media: 0,
},
out: {
date: 0,
date: 1,
album: 0,
media: 1,
},
@ -379,12 +359,12 @@ describe('timeline gallery reducer', () => {
{
name: 'previous date',
in: {
date: 1,
date: 2,
album: 0,
media: 0,
},
out: {
date: 0,
date: 1,
album: 1,
media: 0,
},

View File

@ -1,10 +1,8 @@
import React from 'react'
import {
myTimeline_myTimeline,
myTimeline_myTimeline_media,
} from './__generated__/myTimeline'
import { TimelineGroup } from './TimelineGallery'
import { myTimeline_myTimeline } from './__generated__/myTimeline'
import { TimelineGroup, TimelineGroupAlbum } from './TimelineGallery'
import { GalleryAction } from '../photoGallery/photoGalleryReducer'
import { isNil } from '../../helpers/utils'
export interface TimelineMediaIndex {
date: number
@ -30,18 +28,7 @@ export function timelineGalleryReducer(
): TimelineGalleryState {
switch (action.type) {
case 'replaceTimelineGroups': {
const dateGroupedAlbums = action.timeline.reduce((acc, val) => {
if (acc.length == 0 || acc[acc.length - 1].date != val.date) {
acc.push({
date: val.date,
groups: [val],
})
} else {
acc[acc.length - 1].groups.push(val)
}
return acc
}, [] as TimelineGroup[])
const timelineGroups = convertMediaToTimelineGroups(action.timeline)
return {
...state,
@ -50,7 +37,7 @@ export function timelineGalleryReducer(
date: -1,
media: -1,
},
timelineGroups: dateGroupedAlbums,
timelineGroups,
}
}
case 'nextImage': {
@ -64,7 +51,7 @@ export function timelineGalleryReducer(
return state
}
const albumGroups = timelineGroups[activeIndex.date].groups
const albumGroups = timelineGroups[activeIndex.date].albums
const albumMedia = albumGroups[activeIndex.album].media
if (activeIndex.media < albumMedia.length - 1) {
@ -124,7 +111,7 @@ export function timelineGalleryReducer(
}
if (activeIndex.album > 0) {
const albumGroups = state.timelineGroups[activeIndex.date].groups
const albumGroups = state.timelineGroups[activeIndex.date].albums
const albumMedia = albumGroups[activeIndex.album - 1].media
return {
@ -138,7 +125,7 @@ export function timelineGalleryReducer(
}
if (activeIndex.date > 0) {
const albumGroups = state.timelineGroups[activeIndex.date - 1].groups
const albumGroups = state.timelineGroups[activeIndex.date - 1].albums
const albumMedia = albumGroups[albumGroups.length - 1].media
return {
@ -181,9 +168,9 @@ export const getTimelineImage = ({
}: {
mediaState: TimelineGalleryState
index: TimelineMediaIndex
}): myTimeline_myTimeline_media => {
}): myTimeline_myTimeline => {
const { date, album, media } = index
return mediaState.timelineGroups[date].groups[album].media[media]
return mediaState.timelineGroups[date].albums[album].media[media]
}
export const getActiveTimelineImage = ({
@ -203,6 +190,74 @@ export const getActiveTimelineImage = ({
return getTimelineImage({ mediaState, index: mediaState.activeIndex })
}
function convertMediaToTimelineGroups(
timelineMedia: myTimeline_myTimeline[]
): TimelineGroup[] {
const timelineGroups: TimelineGroup[] = []
let albums: TimelineGroupAlbum[] = []
let nextAlbum: TimelineGroupAlbum | null = null
const sameDay = (a: string, b: string) => {
return (
a.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00') ==
b.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00')
)
}
for (const media of timelineMedia) {
if (nextAlbum == null) {
nextAlbum = {
id: media.album.id,
title: media.album.title,
media: [media],
}
continue
}
// if date changes
if (!sameDay(nextAlbum.media[0].date, media.date)) {
albums.push(nextAlbum)
timelineGroups.push({
date: albums[0].media[0].date.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00'),
albums: albums,
})
albums = []
nextAlbum = {
id: media.album.id,
title: media.album.title,
media: [media],
}
continue
}
// if album changes
if (nextAlbum.id != media.album.id) {
albums.push(nextAlbum)
nextAlbum = {
id: media.album.id,
title: media.album.title,
media: [media],
}
continue
}
// same album and date
nextAlbum.media.push(media)
}
if (!isNil(nextAlbum)) {
albums.push(nextAlbum)
timelineGroups.push({
date: albums[0].media[0].date.replace(/\d{2}:\d{2}:\d{2}/, '00:00:00'),
albums: albums,
})
}
return timelineGroups
}
export const openTimelinePresentMode = ({
dispatchMedia,
activeIndex,

View File

@ -0,0 +1,115 @@
import { MediaType } from '../../__generated__/globalTypes'
import { myTimeline_myTimeline } from './__generated__/myTimeline'
export const timelineData: myTimeline_myTimeline[] = [
{
__typename: 'Media',
id: '1058',
title: '122A2876.jpg',
type: MediaType.Photo,
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_122A2876_jpg_Kp1U80vD.jpg',
width: 1024,
height: 682,
},
highRes: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/122A2876_5cSPMiKL.jpg',
width: 6720,
height: 4480,
},
videoWeb: null,
favorite: false,
album: { __typename: 'Album', id: '522', title: 'random' },
date: '2020-12-13T18:03:40Z',
},
{
__typename: 'Media',
id: '1059',
title: '122A2630-Edit.jpg',
type: MediaType.Photo,
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_122A2630-Edit_jpg_pwjtMkpy.jpg',
width: 1024,
height: 682,
},
highRes: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/122A2630-Edit_ySQWFAgE.jpg',
width: 6177,
height: 4118,
},
videoWeb: null,
favorite: false,
album: { __typename: 'Album', id: '523', title: 'another_album' },
date: '2020-11-25T16:14:33Z',
},
{
__typename: 'Media',
id: '1060',
title: '122A2785-2.jpg',
type: MediaType.Photo,
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_122A2785-2_jpg_CevmxEXf.jpg',
width: 1024,
height: 1024,
},
highRes: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/122A2785-2_mCnWjLdb.jpg',
width: 884,
height: 884,
},
videoWeb: null,
favorite: false,
album: { __typename: 'Album', id: '523', title: 'another_album' },
date: '2020-11-25T16:43:59Z',
},
{
__typename: 'Media',
id: '1056',
title: '122A2630-Edit.jpg',
type: MediaType.Photo,
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_122A2630-Edit_jpg_aJPCSDDl.jpg',
width: 1024,
height: 682,
},
highRes: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/122A2630-Edit_em9g89qg.jpg',
width: 6177,
height: 4118,
},
videoWeb: null,
favorite: false,
album: { __typename: 'Album', id: '522', title: 'random' },
date: '2020-11-25T16:14:33Z',
},
{
__typename: 'Media',
id: '1054',
title: '122A2559.jpg',
type: MediaType.Photo,
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_122A2559_jpg_MsOJtPi8.jpg',
width: 1024,
height: 712,
},
highRes: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/122A2559_FDsQHuBN.jpg',
width: 6246,
height: 4346,
},
videoWeb: null,
favorite: false,
album: { __typename: 'Album', id: '522', title: 'random' },
date: '2020-11-09T15:38:09Z',
},
]

View File

@ -61,17 +61,17 @@
"description": "Simpelt og Brugervenligt Photo-galleri for Personlige Servere"
},
"people_page": {
"action_label": {
"change_label": null,
"detach_face": null,
"merge_face": null,
"move_faces": null
},
"face_group": {
"label_placeholder": "Navn",
"unlabeled": "Ikke navngivet",
"unlabeled_person": "Ikke navngivet person"
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"modal": {
"action": {
"merge": "Sammenflet"
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": "Albumtitel"
},
"download": {
@ -295,6 +299,12 @@
"places": "Kort",
"settings": "Indstillinger"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Loader album",
"login": "Log ind",

View File

@ -36,6 +36,12 @@
"select_image_faces": {
"search_images_placeholder": "Søg billeder..."
}
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
}
},
"settings": {
@ -53,6 +59,14 @@
},
"sharing": {
"table_header": "Offentlige delinger"
},
"download": {
"filesize": {
"giga_byte_plural": null,
"kilo_byte_plural": null,
"mega_byte_plural": null,
"tera_byte_plural": null
}
}
}
}

View File

@ -61,17 +61,17 @@
"description": "Einfache und nutzerfreundliche Fotogallerie für Homeserver"
},
"people_page": {
"action_label": {
"change_label": null,
"detach_face": null,
"merge_face": null,
"move_faces": null
},
"face_group": {
"label_placeholder": "Zuordnung",
"unlabeled": "Nicht zugeordnet",
"unlabeled_person": null
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"modal": {
"action": {
"merge": null
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": null
},
"download": {
@ -295,6 +299,12 @@
"places": "Orte",
"settings": "Einstellungen"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Lade Album",
"login": "Login",

View File

@ -15,7 +15,8 @@
"header": {
"search": {
"result_type": {
"photos": "Fotos"
"photos": "Fotos",
"media": null
}
}
},
@ -69,23 +70,55 @@
"select_image_faces": {
"search_images_placeholder": null
}
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"tableselect_face_group": {
"search_faces_placeholder": null
},
"tableselect_image_faces": {
"search_images_placeholder": null
}
},
"settings": {
"users": {
"table": {
"column_names": {
"admin": "Administrator"
"admin": "Administrator",
"capabilities": null
}
}
}
},
"sidebar": {
"album": {
"title": "Album Optionen"
"title": "Album Optionen",
"title_placeholder": null
},
"sharing": {
"table_header": "Öffentliche Freigabe"
"table_header": "Öffentliche Freigabe",
"delete": null,
"more": null
},
"download": {
"filesize": {
"giga_byte_plural": null,
"kilo_byte_plural": null,
"mega_byte_plural": null,
"tera_byte_plural": null
}
}
},
"album_filter": {
"sort": null
},
"share_page": {
"protected_share": {
"password_required_error": null
}
}
}

View File

@ -61,17 +61,17 @@
"description": "Simple and User-friendly Photo Gallery for Personal Servers"
},
"people_page": {
"action_label": {
"change_label": "Change label",
"detach_face": "Detach face",
"merge_face": "Merge face",
"move_faces": "Move faces"
},
"face_group": {
"label_placeholder": "Label",
"unlabeled": "Unlabeled",
"unlabeled_person": "Unlabeled person"
},
"action_label": {
"change_label": "Change label",
"merge_face": "Merge face",
"detach_face": "Detach face",
"move_faces": "Move faces"
},
"modal": {
"action": {
"merge": "Merge"
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": "Album cover",
"cover_photo": "Album cover",
"reset_cover": "Reset cover photo",
"set_cover": "Set as album cover photo",
"title_placeholder": "Album title"
},
"download": {
@ -295,6 +299,12 @@
"places": "Places",
"settings": "Settings"
},
"timeline_filter": {
"date": {
"dropdown_all": "From today",
"label": "Date"
}
},
"title": {
"loading_album": "Loading album",
"login": "Login",

View File

@ -61,17 +61,17 @@
"description": "Una galería de fotos sencilla y fácil de usar para servidores personales"
},
"people_page": {
"action_label": {
"change_label": null,
"detach_face": null,
"merge_face": null,
"move_faces": null
},
"face_group": {
"label_placeholder": "Etiqueta",
"unlabeled": "Sin etiquetar",
"unlabeled_person": null
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"modal": {
"action": {
"merge": null
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": null
},
"download": {
@ -295,6 +299,12 @@
"places": "Lugares",
"settings": "Opciones"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Cargando álbum",
"login": null,

View File

@ -15,7 +15,8 @@
"header": {
"search": {
"result_type": {
"photos": "Fotos"
"photos": "Fotos",
"media": null
}
}
},
@ -69,13 +70,26 @@
"select_image_faces": {
"search_images_placeholder": null
}
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"tableselect_face_group": {
"search_faces_placeholder": null
},
"tableselect_image_faces": {
"search_images_placeholder": null
}
},
"settings": {
"users": {
"table": {
"column_names": {
"admin": "Administrador"
"admin": "Administrador",
"capabilities": null
}
}
},
@ -87,13 +101,32 @@
},
"sidebar": {
"album": {
"title": "opciones de álbum"
"title": "opciones de álbum",
"title_placeholder": null
},
"sharing": {
"table_header": "Compartidos públicos"
"table_header": "Compartidos públicos",
"delete": null,
"more": null
},
"download": {
"filesize": {
"giga_byte_plural": null,
"kilo_byte_plural": null,
"mega_byte_plural": null,
"tera_byte_plural": null
}
}
},
"title": {
"login": null
},
"album_filter": {
"sort": null
},
"share_page": {
"protected_share": {
"password_required_error": null
}
}
}

View File

@ -61,17 +61,17 @@
"description": "Galerie Photo simple et convivial pour les Serveurs Personnels"
},
"people_page": {
"action_label": {
"change_label": "Changer le label",
"detach_face": "Détacher le visage",
"merge_face": "Fusionner le visage",
"move_faces": "Déplacer les visages"
},
"face_group": {
"label_placeholder": "Étiquette",
"unlabeled": "Sans étiquette",
"unlabeled_person": "Personne sans étiquette"
},
"action_label": {
"change_label": "Changer le label",
"merge_face": "Fusionner le visage",
"detach_face": "Détacher le visage",
"move_faces": "Déplacer les visages"
},
"modal": {
"action": {
"merge": "Fusionner"
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": "Titre de l'Album"
},
"download": {
@ -295,6 +299,12 @@
"places": "Lieux",
"settings": "Réglages"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Chargement de l'album",
"login": "Connexion",

View File

@ -61,17 +61,17 @@
"description": "Galleria fotografica semplice e user-friendly per il tuo server"
},
"people_page": {
"action_label": {
"change_label": null,
"detach_face": null,
"merge_face": null,
"move_faces": null
},
"face_group": {
"label_placeholder": "Etichetta",
"unlabeled": "Senza etichetta",
"unlabeled_person": "Persona senza etichetta"
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"modal": {
"action": {
"merge": "Unisci"
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": null
},
"download": {
@ -295,6 +299,12 @@
"places": "Luoghi",
"settings": "Impostazioni"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Caricamento album",
"login": "Login",

View File

@ -15,7 +15,8 @@
"header": {
"search": {
"result_type": {
"photos": "Foto"
"photos": "Foto",
"media": null
}
}
},
@ -36,23 +37,55 @@
"select_image_faces": {
"search_images_placeholder": "Cerca immagini..."
}
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"tableselect_face_group": {
"search_faces_placeholder": null
},
"tableselect_image_faces": {
"search_images_placeholder": null
}
},
"settings": {
"users": {
"table": {
"column_names": {
"admin": "Admin"
"admin": "Admin",
"capabilities": null
}
}
}
},
"sidebar": {
"album": {
"title": "Opzioni Album"
"title": "Opzioni Album",
"title_placeholder": null
},
"sharing": {
"table_header": "Condivisioni pubbliche"
"table_header": "Condivisioni pubbliche",
"delete": null,
"more": null
},
"download": {
"filesize": {
"giga_byte_plural": null,
"kilo_byte_plural": null,
"mega_byte_plural": null,
"tera_byte_plural": null
}
}
},
"album_filter": {
"sort": null
},
"share_page": {
"protected_share": {
"password_required_error": null
}
}
}

View File

@ -61,17 +61,17 @@
"description": "Prosta i przyjazna dla użytkownika galeria zdjęć"
},
"people_page": {
"action_label": {
"change_label": null,
"detach_face": null,
"merge_face": null,
"move_faces": null
},
"face_group": {
"label_placeholder": "Etykieta",
"unlabeled": "Nieoznakowany",
"unlabeled_person": null
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"modal": {
"action": {
"merge": null
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": null
},
"download": {
@ -300,6 +304,12 @@
"places": "Miejsca",
"settings": "Ustawienia"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Ładowanie albumu",
"login": null,

View File

@ -15,7 +15,8 @@
"header": {
"search": {
"result_type": {
"photos": "Zdjęcia"
"photos": "Zdjęcia",
"media": null
}
}
},
@ -69,13 +70,26 @@
"select_image_faces": {
"search_images_placeholder": null
}
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"tableselect_face_group": {
"search_faces_placeholder": null
},
"tableselect_image_faces": {
"search_images_placeholder": null
}
},
"settings": {
"users": {
"table": {
"column_names": {
"admin": "Admin"
"admin": "Admin",
"capabilities": null
}
}
},
@ -87,7 +101,8 @@
},
"sidebar": {
"album": {
"title": "Opcje albumu"
"title": "Opcje albumu",
"title_placeholder": null
},
"download": {
"filesize": {
@ -99,14 +114,36 @@
"giga_byte": "{{count}} GB",
"kilo_byte": "{{count}} KB",
"mega_byte": "{{count}} MB",
"tera_byte": "{{count}} TB"
"tera_byte": "{{count}} TB",
"giga_byte_0": null,
"giga_byte_1": null,
"giga_byte_2": null,
"kilo_byte_0": null,
"kilo_byte_1": null,
"kilo_byte_2": null,
"mega_byte_0": null,
"mega_byte_1": null,
"mega_byte_2": null,
"tera_byte_0": null,
"tera_byte_1": null,
"tera_byte_2": null
}
},
"sharing": {
"table_header": "Publiczne udostępnienia"
"table_header": "Publiczne udostępnienia",
"delete": null,
"more": null
}
},
"title": {
"login": null
},
"album_filter": {
"sort": null
},
"share_page": {
"protected_share": {
"password_required_error": null
}
}
}

View File

@ -61,17 +61,17 @@
"description": "Простая и Удобная Фото Галерея для Личного Сервера"
},
"people_page": {
"action_label": {
"change_label": null,
"detach_face": null,
"merge_face": null,
"move_faces": null
},
"face_group": {
"label_placeholder": "Метка",
"unlabeled": "Без метки",
"unlabeled_person": "Человек без метки"
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"modal": {
"action": {
"merge": "Объединить"
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": null
},
"download": {
@ -300,6 +304,12 @@
"places": "Места",
"settings": "Настройки"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Загрузка альбома",
"login": "Имя пользователя",

View File

@ -15,7 +15,8 @@
"header": {
"search": {
"result_type": {
"photos": "Фото"
"photos": "Фото",
"media": null
}
}
},
@ -36,20 +37,34 @@
"select_image_faces": {
"search_images_placeholder": "Поиск фото..."
}
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"tableselect_face_group": {
"search_faces_placeholder": null
},
"tableselect_image_faces": {
"search_images_placeholder": null
}
},
"settings": {
"users": {
"table": {
"column_names": {
"admin": "Администрирование"
"admin": "Администрирование",
"capabilities": null
}
}
}
},
"sidebar": {
"album": {
"title": "Свойства альбома"
"title": "Свойства альбома",
"title_placeholder": null
},
"download": {
"filesize": {
@ -58,11 +73,36 @@
"giga_byte": "{{count}} ГБ",
"kilo_byte": "{{count}} КБ",
"mega_byte": "{{count}} МБ",
"tera_byte": "{{count}} ТБ"
"tera_byte": "{{count}} ТБ",
"byte_0": null,
"byte_1": null,
"byte_2": null,
"giga_byte_0": null,
"giga_byte_1": null,
"giga_byte_2": null,
"kilo_byte_0": null,
"kilo_byte_1": null,
"kilo_byte_2": null,
"mega_byte_0": null,
"mega_byte_1": null,
"mega_byte_2": null,
"tera_byte_0": null,
"tera_byte_1": null,
"tera_byte_2": null
}
},
"sharing": {
"table_header": "Общий доступ"
"table_header": "Общий доступ",
"delete": null,
"more": null
}
},
"album_filter": {
"sort": null
},
"share_page": {
"protected_share": {
"password_required_error": null
}
}
}

View File

@ -61,17 +61,17 @@
"description": "Enkelt och Användarvänligt fotogalleri för personliga servrar"
},
"people_page": {
"action_label": {
"change_label": null,
"detach_face": null,
"merge_face": null,
"move_faces": null
},
"face_group": {
"label_placeholder": "Märkning",
"unlabeled": "Omärkt",
"unlabeled_person": null
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"modal": {
"action": {
"merge": null
@ -215,6 +215,10 @@
},
"sidebar": {
"album": {
"album_cover": null,
"cover_photo": null,
"reset_cover": null,
"set_cover": null,
"title_placeholder": null
},
"download": {
@ -295,6 +299,12 @@
"places": "Platser",
"settings": "Inställningar"
},
"timeline_filter": {
"date": {
"dropdown_all": null,
"label": null
}
},
"title": {
"loading_album": "Laddar album",
"login": null,

View File

@ -15,7 +15,8 @@
"header": {
"search": {
"result_type": {
"photos": "Bilder"
"photos": "Bilder",
"media": null
}
}
},
@ -69,6 +70,18 @@
"select_image_faces": {
"search_images_placeholder": null
}
},
"action_label": {
"change_label": null,
"merge_face": null,
"detach_face": null,
"move_faces": null
},
"tableselect_face_group": {
"search_faces_placeholder": null
},
"tableselect_image_faces": {
"search_images_placeholder": null
}
},
"places_page": {
@ -78,7 +91,8 @@
"users": {
"table": {
"column_names": {
"admin": "Admin"
"admin": "Admin",
"capabilities": null
}
}
},
@ -90,13 +104,32 @@
},
"sidebar": {
"album": {
"title": "Albuminställningar"
"title": "Albuminställningar",
"title_placeholder": null
},
"sharing": {
"table_header": "Publika delningar"
"table_header": "Publika delningar",
"delete": null,
"more": null
},
"download": {
"filesize": {
"giga_byte_plural": null,
"kilo_byte_plural": null,
"mega_byte_plural": null,
"tera_byte_plural": null
}
}
},
"title": {
"login": null
},
"album_filter": {
"sort": null
},
"share_page": {
"protected_share": {
"password_required_error": null
}
}
}

View File

@ -90,6 +90,10 @@ function useScrollPagination<D>({
reconfigureIntersectionObserver()
}, [fetchMore, data, finished])
useEffect(() => {
setFinished(false)
}, [data])
return {
containerElem,
finished,

View File

@ -1,10 +1,10 @@
import { useState } from 'react'
export type UrlKeyValuePair = { key: string; value: string }
export type UrlKeyValuePair = { key: string; value: string | null }
export type UrlParams = {
getParam(key: string, defaultValue?: string | null): string | null
setParam(key: string, value: string): void
setParam(key: string, value: string | null): void
setParams(pairs: UrlKeyValuePair[]): void
}
@ -19,18 +19,30 @@ function useURLParameters(): UrlParams {
}
const updateParams = () => {
history.replaceState({}, '', url.pathname + '?' + params.toString())
if (params.toString()) {
history.replaceState({}, '', url.pathname + '?' + params.toString())
} else {
history.replaceState({}, '', url.pathname)
}
setUrlString(document.location.href)
}
const setParam = (key: string, value: string) => {
params.set(key, value)
const setParam = (key: string, value: string | null) => {
if (value) {
params.set(key, value)
} else {
params.delete(key)
}
updateParams()
}
const setParams = (pairs: UrlKeyValuePair[]) => {
for (const pair of pairs) {
params.set(pair.key, pair.value)
if (pair.value) {
params.set(pair.key, pair.value)
} else {
params.delete(pair.key)
}
}
updateParams()
}