From 4fdb9357ea8f0abf2f79f0c0ded6e0b88da06113 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 7 Feb 2021 17:13:27 +0100 Subject: [PATCH] Add favorites filter for timeline --- api/graphql/generated.go | 37 ++++++++++++++++--- api/graphql/resolvers/timeline.go | 35 +++++++++++------- api/graphql/schema.graphql | 2 +- ui/src/components/AlbumFilter.js | 23 +++++++++--- .../timelineGallery/TimelineGallery.js | 30 ++++++++++++--- .../timelineGallery/TimelineGroupAlbum.js | 12 ++++-- .../timelineGallery/TimelineGroupDate.js | 8 +++- ui/src/components/useURLParameters.js | 26 +++++++++++++ 8 files changed, 138 insertions(+), 35 deletions(-) create mode 100644 ui/src/components/useURLParameters.js diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 33471fee..5fec4e7f 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -152,7 +152,7 @@ type ComplexityRoot struct { MyAlbums func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int MyMedia func(childComplexity int, filter *models.Filter) int MyMediaGeoJSON func(childComplexity int) int - MyTimeline func(childComplexity int) int + MyTimeline func(childComplexity int, onlyFavorites *bool) int MyUser func(childComplexity int) int Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int ShareToken func(childComplexity int, token string, password *string) int @@ -272,7 +272,7 @@ type QueryResolver interface { MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error) Media(ctx context.Context, id int) (*models.Media, error) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) - MyTimeline(ctx context.Context) ([]*models.TimelineGroup, error) + MyTimeline(ctx context.Context, onlyFavorites *bool) ([]*models.TimelineGroup, error) MyMediaGeoJSON(ctx context.Context) (interface{}, error) MapboxToken(ctx context.Context) (*string, error) ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) @@ -945,7 +945,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in break } - return e.complexity.Query.MyTimeline(childComplexity), true + args, err := ec.field_Query_myTimeline_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.MyTimeline(childComplexity, args["onlyFavorites"].(*bool)), true case "Query.myUser": if e.complexity.Query.MyUser == nil { @@ -1395,7 +1400,7 @@ type Query { "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! - myTimeline: [TimelineGroup!]! + myTimeline(onlyFavorites: Boolean): [TimelineGroup!]! "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! @@ -2187,6 +2192,21 @@ func (ec *executionContext) field_Query_myMedia_args(ctx context.Context, rawArg return args, nil } +func (ec *executionContext) field_Query_myTimeline_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 *bool + if tmp, ok := rawArgs["onlyFavorites"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyFavorites")) + arg0, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) + if err != nil { + return nil, err + } + } + args["onlyFavorites"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_search_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -5205,9 +5225,16 @@ func (ec *executionContext) _Query_myTimeline(ctx context.Context, field graphql } ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_myTimeline_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyTimeline(rctx) + return ec.resolvers.Query().MyTimeline(rctx, args["onlyFavorites"].(*bool)) }) if err != nil { ec.Error(ctx, err) diff --git a/api/graphql/resolvers/timeline.go b/api/graphql/resolvers/timeline.go index 48820774..5fcdb3ed 100644 --- a/api/graphql/resolvers/timeline.go +++ b/api/graphql/resolvers/timeline.go @@ -4,17 +4,22 @@ import ( "context" "time" + "github.com/photoview/photoview/api/graphql/auth" "github.com/photoview/photoview/api/graphql/models" "gorm.io/gorm" ) -func (r *queryResolver) MyTimeline(ctx context.Context) ([]*models.TimelineGroup, error) { +func (r *queryResolver) MyTimeline(ctx context.Context, onlyFavorites *bool) ([]*models.TimelineGroup, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } var timelineGroups []*models.TimelineGroup transactionError := r.Database.Transaction(func(tx *gorm.DB) error { // album_id, year, month, day - rows, err := tx.Select( + daysQuery := tx.Select( "albums.id AS album_id", "YEAR(media.date_shot) AS year", "MONTH(media.date_shot) AS month", @@ -22,7 +27,13 @@ func (r *queryResolver) MyTimeline(ctx context.Context) ([]*models.TimelineGroup ). Table("media"). Joins("JOIN albums ON media.album_id = albums.id"). - Group("albums.id, YEAR(media.date_shot), MONTH(media.date_shot), DAY(media.date_shot)"). + 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")) + } + + rows, err := daysQuery.Group("albums.id, YEAR(media.date_shot), MONTH(media.date_shot), DAY(media.date_shot)"). Order("media.date_shot DESC"). Rows() @@ -59,23 +70,21 @@ func (r *queryResolver) MyTimeline(ctx context.Context) ([]*models.TimelineGroup // Fill media var groupMedia []*models.Media - err := tx.Model(&models.Media{}). + mediaQuery := tx.Model(&models.Media{}). Where("album_id = ? AND YEAR(date_shot) = ? AND MONTH(date_shot) = ? AND DAY(date_shot) = ?", group.albumID, group.year, group.month, group.day). - Order("date_shot DESC"). - Limit(5). - Find(&groupMedia).Error + Order("date_shot DESC") - if err != nil { + 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 - err = tx.Model(&models.Media{}). - Where("album_id = ? AND YEAR(date_shot) = ? AND MONTH(date_shot) = ? AND DAY(date_shot) = ?", group.albumID, group.year, group.month, group.day). - Count(&totalMedia).Error - - if err != nil { + if err := mediaQuery.Count(&totalMedia).Error; err != nil { return err } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 4780e111..0dddd38f 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -44,7 +44,7 @@ type Query { "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! - myTimeline: [TimelineGroup!]! + myTimeline(onlyFavorites: Boolean): [TimelineGroup!]! "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! diff --git a/ui/src/components/AlbumFilter.js b/ui/src/components/AlbumFilter.js index c71ca460..9766624a 100644 --- a/ui/src/components/AlbumFilter.js +++ b/ui/src/components/AlbumFilter.js @@ -27,7 +27,7 @@ const sortingOptions = [ }, ] -const FavoritesCheckbox = styled(Checkbox)` +const FavoritesCheckboxStyle = styled(Checkbox)` margin-bottom: 16px; margin-right: 10px; @@ -56,6 +56,20 @@ const FavoritesCheckbox = styled(Checkbox)` } ` +export const FavoritesCheckbox = ({ onlyFavorites, setOnlyFavorites }) => ( + setOnlyFavorites(result.checked)} + /> +) + +FavoritesCheckbox.propTypes = { + onlyFavorites: PropTypes.bool.isRequired, + setOnlyFavorites: PropTypes.func.isRequired, +} + const OrderDirectionButton = styled(Button)` padding: 0.88em; margin-left: 10px !important; @@ -82,11 +96,8 @@ const AlbumFilter = ({ <> {authToken() && ( e.stopPropagation()} - onChange={(e, result) => setOnlyFavorites(result.checked)} + onlyFavorites={onlyFavorites} + setOnlyFavorites={setOnlyFavorites} /> )} Sort by diff --git a/ui/src/components/timelineGallery/TimelineGallery.js b/ui/src/components/timelineGallery/TimelineGallery.js index 657df6db..d67a73ca 100644 --- a/ui/src/components/timelineGallery/TimelineGallery.js +++ b/ui/src/components/timelineGallery/TimelineGallery.js @@ -4,10 +4,13 @@ import { useQuery, gql } from '@apollo/client' import TimelineGroupDate from './TimelineGroupDate' import styled from 'styled-components' import PresentView from '../photoGallery/presentView/PresentView' +import { Loader } from 'semantic-ui-react' +import useURLParameters from '../useUrlParameters' +import { FavoritesCheckbox } from '../AlbumFilter' const MY_TIMELINE_QUERY = gql` - query myTimeline { - myTimeline { + query myTimeline($onlyFavorites: Boolean) { + myTimeline(onlyFavorites: $onlyFavorites) { album { id title @@ -38,6 +41,7 @@ const MY_TIMELINE_QUERY = gql` ` const GalleryWrapper = styled.div` + margin: -12px; display: flex; flex-wrap: wrap; ` @@ -50,6 +54,11 @@ const TimelineGallery = () => { }) const [presenting, setPresenting] = useState(false) + const { getParam, setParam } = useURLParameters() + + const onlyFavorites = getParam('favorites') == '1' ? true : false + const setOnlyFavorites = favorites => setParam('favorites', favorites ? 1 : 0) + const nextMedia = useCallback(() => { setActiveIndex(activeIndex => { const albumGroups = dateGroupedAlbums[activeIndex.dateGroup].groups @@ -113,7 +122,11 @@ const TimelineGallery = () => { }) }, [activeIndex]) - const { data, error } = useQuery(MY_TIMELINE_QUERY) + const { data, error, loading } = useQuery(MY_TIMELINE_QUERY, { + variables: { + onlyFavorites, + }, + }) if (error) { return error @@ -158,8 +171,13 @@ const TimelineGallery = () => { } return ( - - {timelineGroups} +
+ Loading timeline + + {timelineGroups} {presenting && ( { setPresenting={setPresenting} /> )} - +
) } diff --git a/ui/src/components/timelineGallery/TimelineGroupAlbum.js b/ui/src/components/timelineGallery/TimelineGroupAlbum.js index 680044c6..446de523 100644 --- a/ui/src/components/timelineGallery/TimelineGroupAlbum.js +++ b/ui/src/components/timelineGallery/TimelineGroupAlbum.js @@ -4,6 +4,7 @@ import { MediaThumbnail } from '../photoGallery/MediaThumbnail' import styled from 'styled-components' import { SidebarContext } from '../sidebar/Sidebar' import MediaSidebar from '../sidebar/MediaSidebar' +import { Link } from 'react-router-dom' const MediaWrapper = styled.div` display: flex; @@ -22,14 +23,17 @@ const MediaWrapper = styled.div` ` const AlbumTitle = styled.h2` - color: #212121; font-size: 1.25rem; font-weight: 200; margin: 0 0 4px; + + & a:not(:hover) { + color: #212121; + } ` const GroupAlbumWrapper = styled.div` - margin-top: 12px; + margin: 12px 8px 0; ` const TimelineGroupAlbum = ({ @@ -56,7 +60,9 @@ const TimelineGroupAlbum = ({ return ( - {album.title} + + {album.title} + {mediaElms} ) diff --git a/ui/src/components/timelineGallery/TimelineGroupDate.js b/ui/src/components/timelineGallery/TimelineGroupDate.js index 7a2de68d..5a8b6517 100644 --- a/ui/src/components/timelineGallery/TimelineGroupDate.js +++ b/ui/src/components/timelineGallery/TimelineGroupDate.js @@ -18,6 +18,12 @@ const DateTitle = styled.h1` margin: 0 0 -12px; ` +const GroupAlbumWrapper = styled.div` + display: flex; + flex-wrap: wrap; + margin: 0 -8px; +` + const TimelineGroupDate = ({ date, groups, @@ -45,7 +51,7 @@ const TimelineGroupDate = ({ return ( {formattedDate} -
{albumGroupElms}
+ {albumGroupElms}
) } diff --git a/ui/src/components/useURLParameters.js b/ui/src/components/useURLParameters.js new file mode 100644 index 00000000..d68687db --- /dev/null +++ b/ui/src/components/useURLParameters.js @@ -0,0 +1,26 @@ +import { useState } from 'react' + +function useURLParameters() { + const [urlString, setUrlString] = useState(document.location.href) + + const url = new URL(urlString) + const params = new URLSearchParams(url.search) + + const getParam = key => { + return params.get(key) + } + + const setParam = (key, value) => { + params.set(key, value) + history.replaceState({}, '', url.pathname + '?' + params.toString()) + + setUrlString(document.location.href) + } + + return { + getParam, + setParam, + } +} + +export default useURLParameters