1
Fork 0

Merge branch 'master' into geographic-map-page

This commit is contained in:
viktorstrate 2020-09-27 20:08:43 +02:00
commit db64d3eb1b
10 changed files with 277 additions and 57 deletions

View File

@ -54,7 +54,7 @@ type ComplexityRoot struct {
Album struct {
FilePath func(childComplexity int) int
ID func(childComplexity int) int
Media func(childComplexity int, filter *models.Filter) int
Media func(childComplexity int, filter *models.Filter, onlyFavorites *bool) int
Owner func(childComplexity int) int
ParentAlbum func(childComplexity int) int
Path func(childComplexity int) int
@ -146,7 +146,7 @@ type ComplexityRoot struct {
Album func(childComplexity int, id int) int
MapboxToken func(childComplexity int) int
Media func(childComplexity int, id int) int
MyAlbums func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool) int
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
MyUser func(childComplexity int) int
@ -212,7 +212,7 @@ type ComplexityRoot struct {
}
type AlbumResolver interface {
Media(ctx context.Context, obj *models.Album, filter *models.Filter) ([]*models.Media, error)
Media(ctx context.Context, obj *models.Album, filter *models.Filter, onlyFavorites *bool) ([]*models.Media, error)
SubAlbums(ctx context.Context, obj *models.Album, filter *models.Filter) ([]*models.Album, error)
ParentAlbum(ctx context.Context, obj *models.Album) (*models.Album, error)
Owner(ctx context.Context, obj *models.Album) (*models.User, error)
@ -253,7 +253,7 @@ type QueryResolver interface {
SiteInfo(ctx context.Context) (*models.SiteInfo, error)
User(ctx context.Context, filter *models.Filter) ([]*models.User, error)
MyUser(ctx context.Context) (*models.User, error)
MyAlbums(ctx context.Context, filter *models.Filter, onlyRoot *bool, showEmpty *bool) ([]*models.Album, error)
MyAlbums(ctx context.Context, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error)
Album(ctx context.Context, id int) (*models.Album, error)
MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error)
Media(ctx context.Context, id int) (*models.Media, error)
@ -313,7 +313,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Album.Media(childComplexity, args["filter"].(*models.Filter)), true
return e.complexity.Album.Media(childComplexity, args["filter"].(*models.Filter), args["onlyFavorites"].(*bool)), true
case "Album.owner":
if e.complexity.Album.Owner == nil {
@ -879,7 +879,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Query.MyAlbums(childComplexity, args["filter"].(*models.Filter), args["onlyRoot"].(*bool), args["showEmpty"].(*bool)), true
return e.complexity.Query.MyAlbums(childComplexity, args["filter"].(*models.Filter), args["onlyRoot"].(*bool), args["showEmpty"].(*bool), args["onlyWithFavorites"].(*bool)), true
case "Query.myMedia":
if e.complexity.Query.MyMedia == nil {
@ -1299,6 +1299,8 @@ type Query {
onlyRoot: Boolean
"Return also albums with no media directly in them"
showEmpty: Boolean
"Show only albums having favorites"
onlyWithFavorites: Boolean
): [Album!]!
"Get album by id, user must own the album or be admin"
album(id: Int!): Album!
@ -1454,7 +1456,11 @@ type Album {
id: Int!
title: String!
"The media inside this album"
media(filter: Filter): [Media!]!
media(
filter: Filter,
"Return only the favorited media"
onlyFavorites: Boolean
): [Media!]!
"The albums contained in this album"
subAlbums(filter: Filter): [Album!]!
"The album witch contains this album"
@ -1576,6 +1582,14 @@ func (ec *executionContext) field_Album_media_args(ctx context.Context, rawArgs
}
}
args["filter"] = arg0
var arg1 *bool
if tmp, ok := rawArgs["onlyFavorites"]; ok {
arg1, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
if err != nil {
return nil, err
}
}
args["onlyFavorites"] = arg1
return args, nil
}
@ -2041,6 +2055,14 @@ func (ec *executionContext) field_Query_myAlbums_args(ctx context.Context, rawAr
}
}
args["showEmpty"] = arg2
var arg3 *bool
if tmp, ok := rawArgs["onlyWithFavorites"]; ok {
arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
if err != nil {
return nil, err
}
}
args["onlyWithFavorites"] = arg3
return args, nil
}
@ -2288,7 +2310,7 @@ func (ec *executionContext) _Album_media(ctx context.Context, field graphql.Coll
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.Album().Media(rctx, obj, args["filter"].(*models.Filter))
return ec.resolvers.Album().Media(rctx, obj, args["filter"].(*models.Filter), args["onlyFavorites"].(*bool))
})
if err != nil {
ec.Error(ctx, err)
@ -4800,7 +4822,7 @@ func (ec *executionContext) _Query_myAlbums(ctx context.Context, field graphql.C
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().MyAlbums(rctx, args["filter"].(*models.Filter), args["onlyRoot"].(*bool), args["showEmpty"].(*bool))
return ec.resolvers.Query().MyAlbums(rctx, args["filter"].(*models.Filter), args["onlyRoot"].(*bool), args["showEmpty"].(*bool), args["onlyWithFavorites"].(*bool))
})
if err != nil {
ec.Error(ctx, err)

View File

@ -9,7 +9,7 @@ import (
"github.com/viktorstrate/photoview/api/graphql/models"
)
func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onlyRoot *bool, showEmpty *bool) ([]*models.Album, error) {
func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
@ -22,8 +22,13 @@ func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onl
var rows *sql.Rows
filterEmpty := " AND EXISTS (SELECT * FROM media WHERE album_id = album.album_id) "
if showEmpty != nil && *showEmpty == true {
filterFavorites := " AND favorite = 1"
if onlyWithFavorites == nil || *onlyWithFavorites == false {
filterFavorites = ""
}
filterEmpty := " AND EXISTS (SELECT * FROM media WHERE album_id = album.album_id" + filterFavorites + ") "
if showEmpty != nil && *showEmpty == true && (onlyWithFavorites == nil || *onlyWithFavorites == false) {
filterEmpty = ""
}
@ -72,20 +77,25 @@ func (r *Resolver) Album() api.AlbumResolver {
type albumResolver struct{ *Resolver }
func (r *albumResolver) Media(ctx context.Context, obj *models.Album, filter *models.Filter) ([]*models.Media, error) {
func (r *albumResolver) Media(ctx context.Context, obj *models.Album, filter *models.Filter, onlyFavorites *bool) ([]*models.Media, error) {
filterSQL, err := filter.FormatSQL()
if err != nil {
return nil, err
}
filterFavorites := " AND media.favorite = 1 "
if onlyFavorites == nil || *onlyFavorites == false {
filterFavorites = ""
}
mediaRows, err := r.Database.Query(`
SELECT media.* FROM album, media
WHERE album.album_id = ? AND media.album_id = album.album_id
AND media.media_id IN (
SELECT media_id FROM media_url WHERE media_url.media_id = media.media_id
)
`+filterSQL, obj.AlbumID)
`+filterFavorites+filterSQL, obj.AlbumID)
if err != nil {
return nil, err
}

View File

@ -30,6 +30,8 @@ type Query {
onlyRoot: Boolean
"Return also albums with no media directly in them"
showEmpty: Boolean
"Show only albums having favorites"
onlyWithFavorites: Boolean
): [Album!]!
"Get album by id, user must own the album or be admin"
album(id: Int!): Album!
@ -185,7 +187,11 @@ type Album {
id: Int!
title: String!
"The media inside this album"
media(filter: Filter): [Media!]!
media(
filter: Filter,
"Return only the favorited media"
onlyFavorites: Boolean
): [Media!]!
"The albums contained in this album"
subAlbums(filter: Filter): [Album!]!
"The album witch contains this album"

View File

@ -1,11 +1,12 @@
import React, { Component } from 'react'
import React, { useCallback, useState } from 'react'
import ReactRouterPropTypes from 'react-router-prop-types'
import gql from 'graphql-tag'
import { Query } from 'react-apollo'
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
import PropTypes from 'prop-types'
const albumQuery = gql`
query albumQuery($id: Int!) {
query albumQuery($id: Int!, $onlyFavorites: Boolean) {
album(id: $id) {
id
title
@ -18,7 +19,10 @@ const albumQuery = gql`
}
}
}
media(filter: { order_by: "title", order_direction: DESC }) {
media(
filter: { order_by: "title", order_direction: DESC }
onlyFavorites: $onlyFavorites
) {
id
type
thumbnail {
@ -38,15 +42,60 @@ const albumQuery = gql`
}
`
let refetchNeededAll = false
let refetchNeededFavorites = false
function AlbumPage({ match }) {
const albumId = match.params.id
const [onlyFavorites, setOnlyFavorites] = useState(
match.params.subPage === 'favorites'
)
const toggleFavorites = useCallback(
refetch => {
const newState = !onlyFavorites
if (
(refetchNeededAll && !newState) ||
(refetchNeededFavorites && newState)
) {
refetch({ id: albumId, onlyFavorites: newState }).then(() => {
if (onlyFavorites) {
refetchNeededFavorites = false
} else {
refetchNeededAll = false
}
setOnlyFavorites(newState)
})
} else {
setOnlyFavorites(newState)
}
history.replaceState(
{},
'',
'/album/' + albumId + (newState ? '/favorites' : '')
)
},
[onlyFavorites, setOnlyFavorites]
)
return (
<Query query={albumQuery} variables={{ id: albumId }}>
{({ loading, error, data }) => {
<Query query={albumQuery} variables={{ id: albumId, onlyFavorites }}>
{({ loading, error, data, refetch }) => {
if (error) return <div>Error</div>
return <AlbumGallery album={data && data.album} loading={loading} />
return (
<AlbumGallery
album={data && data.album}
loading={loading}
showFavoritesToggle
setOnlyFavorites={() => {
toggleFavorites(refetch)
}}
onlyFavorites={onlyFavorites}
onFavorite={() =>
(refetchNeededAll = refetchNeededFavorites = true)
}
/>
)
}}
</Query>
)
@ -54,6 +103,12 @@ function AlbumPage({ match }) {
AlbumPage.propTypes = {
...ReactRouterPropTypes,
match: PropTypes.shape({
params: PropTypes.shape({
id: PropTypes.string,
subPage: PropTypes.string,
}),
}),
}
export default AlbumPage

View File

@ -4,14 +4,22 @@ import gql from 'graphql-tag'
import { Query } from 'react-apollo'
import PhotoGallery from '../../components/photoGallery/PhotoGallery'
import AlbumTitle from '../../components/AlbumTitle'
import { Checkbox } from 'semantic-ui-react'
import styled from 'styled-components'
import { authToken } from '../../authentication'
import PropTypes from 'prop-types'
const photoQuery = gql`
query allPhotosPage {
myAlbums(filter: { order_by: "title", order_direction: ASC, limit: 100 }) {
query allPhotosPage($onlyWithFavorites: Boolean) {
myAlbums(
filter: { order_by: "title", order_direction: ASC, limit: 100 }
onlyWithFavorites: $onlyWithFavorites
) {
title
id
media(
filter: { order_by: "media.title", order_direction: DESC, limit: 12 }
onlyFavorites: $onlyWithFavorites
) {
id
title
@ -35,6 +43,10 @@ const photoQuery = gql`
}
`
const FavoritesCheckbox = styled(Checkbox)`
margin: 0.5rem 0 0 0;
`
class PhotosPage extends Component {
constructor(props) {
super(props)
@ -43,6 +55,7 @@ class PhotosPage extends Component {
activeAlbumIndex: -1,
activePhotoIndex: -1,
presenting: false,
onlyWithFavorites: this.props.match.params.subPage === 'favorites',
}
this.setPresenting = this.setPresenting.bind(this)
@ -50,6 +63,37 @@ class PhotosPage extends Component {
this.previousImage = this.previousImage.bind(this)
this.albums = []
this.refetchNeededFavorites = false
this.refetchNeededAll = false
}
favoritesCheckboxClick(refetch) {
const onlyWithFavorites = !this.state.onlyWithFavorites
history.replaceState(
{},
'',
'/photos' + (onlyWithFavorites ? '/favorites' : '')
)
if (
(this.refetchNeededAll && !onlyWithFavorites) ||
(this.refetchNeededFavorites && onlyWithFavorites)
) {
refetch({ onlyWithFavorites }).then(() => {
if (onlyWithFavorites) {
this.refetchNeededFavorites = false
} else {
this.refetchNeededAll = false
}
this.setState({
onlyWithFavorites,
})
})
} else {
this.setState({
onlyWithFavorites,
})
}
}
setActiveImage(album, photo) {
@ -91,19 +135,35 @@ class PhotosPage extends Component {
}
render() {
const showOnlyWithFavorites = this.state.onlyWithFavorites
return (
<Layout title="Photos">
<Query query={photoQuery}>
{({ loading, error, data }) => {
<Query
query={photoQuery}
variables={{ onlyWithFavorites: showOnlyWithFavorites }}
>
{({ loading, error, data, refetch }) => {
if (error) return error
if (loading) return null
let galleryGroups = []
let favoritesSwitch = ''
this.albums = data.myAlbums
if (data.myAlbums) {
if (data.myAlbums && authToken()) {
favoritesSwitch = (
<FavoritesCheckbox
toggle
label="Show only the favorites"
onClick={e => e.stopPropagation()}
checked={showOnlyWithFavorites}
onChange={() => {
this.favoritesCheckboxClick(refetch)
}}
/>
)
galleryGroups = data.myAlbums.map((album, index) => (
<div key={album.id}>
<AlbumTitle album={album} />
@ -111,6 +171,10 @@ class PhotosPage extends Component {
onSelectImage={photoIndex => {
this.setActiveImage(index, photoIndex)
}}
onFavorite={() => {
this.refetchNeededAll = true
this.refetchNeededFavorites = true
}}
activeIndex={
this.state.activeAlbumIndex == index
? this.state.activePhotoIndex
@ -129,15 +193,12 @@ class PhotosPage extends Component {
))
}
let activeImage = null
if (this.state.activeAlbumIndex != -1) {
activeImage =
data.myAlbums[this.state.activeAlbumIndex].media[
this.state.activePhotoIndex
].id
}
return <div>{galleryGroups}</div>
return (
<div>
{favoritesSwitch}
{galleryGroups}
</div>
)
}}
</Query>
</Layout>
@ -145,4 +206,12 @@ class PhotosPage extends Component {
}
}
PhotosPage.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
subPage: PropTypes.string,
}),
}),
}
export default PhotosPage

View File

@ -42,8 +42,8 @@ class Routes extends React.Component {
<Route path="/initialSetup" component={InitialSetupPage} />
<Route path="/share" component={SharePage} />
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
<AuthorizedRoute path="/photos" component={PhotosPage} />
<AuthorizedRoute path="/album/:id/:subPage?" component={AlbumPage} />
<AuthorizedRoute path="/photos/:subPage?" component={PhotosPage} />
<AuthorizedRoute path="/places" component={PlacesPage} />
<AuthorizedRoute admin path="/settings" component={SettingsPage} />
<Route path="/" exact render={() => <Redirect to="/photos" />} />

View File

@ -1,6 +1,6 @@
import React, { useEffect, useContext } from 'react'
import PropTypes from 'prop-types'
import { Breadcrumb } from 'semantic-ui-react'
import { Breadcrumb, Checkbox } from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { Icon } from 'semantic-ui-react'
@ -33,6 +33,10 @@ const StyledIcon = styled(Icon)`
}
`
const FavoritesCheckbox = styled(Checkbox)`
margin-bottom: 16px;
`
const SettingsIcon = props => {
return <StyledIcon name="settings" size="small" {...props} />
}
@ -49,7 +53,13 @@ const ALBUM_PATH_QUERY = gql`
}
`
const AlbumTitle = ({ album, disableLink = false }) => {
const AlbumTitle = ({
album,
disableLink = false,
showFavoritesToggle,
setOnlyFavorites,
onlyFavorites = false,
}) => {
const [fetchPath, { data: pathData }] = useLazyQuery(ALBUM_PATH_QUERY)
const { updateSidebar } = useContext(SidebarContext)
@ -91,23 +101,37 @@ const AlbumTitle = ({ album, disableLink = false }) => {
}
return (
<Header>
<Breadcrumb>{breadcrumbSections}</Breadcrumb>
{title}
{authToken() && (
<SettingsIcon
onClick={() => {
updateSidebar(<AlbumSidebar albumId={album.id} />)
}}
<>
<Header>
<Breadcrumb>{breadcrumbSections}</Breadcrumb>
{title}
{authToken() && (
<SettingsIcon
onClick={() => {
updateSidebar(<AlbumSidebar albumId={album.id} />)
}}
/>
)}
</Header>
{authToken() && showFavoritesToggle && (
<FavoritesCheckbox
toggle
label="Show only the favorites"
checked={onlyFavorites}
onClick={e => e.stopPropagation()}
onChange={setOnlyFavorites}
/>
)}
</Header>
</>
)
}
AlbumTitle.propTypes = {
album: PropTypes.object,
disableLink: PropTypes.bool,
showFavoritesToggle: PropTypes.bool,
setOnlyFavorites: PropTypes.func,
onlyFavorites: PropTypes.bool,
}
export default AlbumTitle

View File

@ -5,7 +5,15 @@ import AlbumTitle from '../AlbumTitle'
import PhotoGallery from '../photoGallery/PhotoGallery'
import AlbumBoxes from './AlbumBoxes'
const AlbumGallery = ({ album, loading = false, customAlbumLink }) => {
const AlbumGallery = ({
album,
loading = false,
customAlbumLink,
showFavoritesToggle = false,
setOnlyFavorites,
onlyFavorites = false,
onFavorite,
}) => {
const [imageState, setImageState] = useState({
activeImage: -1,
presenting: false,
@ -78,7 +86,13 @@ const AlbumGallery = ({ album, loading = false, customAlbumLink }) => {
return (
<Layout title={album ? album.title : 'Loading album'}>
<AlbumTitle album={album} disableLink />
<AlbumTitle
album={album}
disableLink
showFavoritesToggle={showFavoritesToggle}
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
/>
{subAlbumElement}
{
<h2
@ -98,6 +112,7 @@ const AlbumGallery = ({ album, loading = false, customAlbumLink }) => {
onSelectImage={index => {
setActiveImage(index)
}}
onFavorite={onFavorite}
setPresenting={setPresentingWithHistory}
nextImage={nextImage}
previousImage={previousImage}
@ -110,6 +125,10 @@ AlbumGallery.propTypes = {
album: PropTypes.object,
loading: PropTypes.bool,
customAlbumLink: PropTypes.func,
showFavoritesToggle: PropTypes.bool,
setOnlyFavorites: PropTypes.func,
onlyFavorites: PropTypes.bool,
onFavorite: PropTypes.func,
}
export default AlbumGallery

View File

@ -130,6 +130,7 @@ export const MediaThumbnail = ({
index,
active,
setPresenting,
onFavorite,
}) => {
const [markFavorite] = useMutation(markFavoriteMutation)
@ -141,19 +142,21 @@ export const MediaThumbnail = ({
name={media.favorite ? 'heart' : 'heart outline'}
onClick={event => {
event.stopPropagation()
const favorite = !media.favorite
markFavorite({
variables: {
mediaId: media.id,
favorite: !media.favorite,
favorite: favorite,
},
optimisticResponse: {
favoritePhoto: {
favoriteMedia: {
id: media.id,
favorite: !media.favorite,
__typename: 'Photo',
favorite: favorite,
__typename: 'Media',
},
},
})
onFavorite()
}}
/>
)

View File

@ -6,6 +6,7 @@ import PresentView from './presentView/PresentView'
import PropTypes from 'prop-types'
import { SidebarContext } from '../sidebar/Sidebar'
import MediaSidebar from '../sidebar/MediaSidebar'
import { forceVisible } from 'react-lazyload'
const Gallery = styled.div`
display: flex;
@ -26,6 +27,10 @@ const PhotoFiller = styled.div`
flex-grow: 999999;
`
const ClearWrap = styled.div`
clear: both;
`
const PhotoGallery = ({
activeIndex = -1,
media,
@ -35,6 +40,7 @@ const PhotoGallery = ({
setPresenting,
nextImage,
previousImage,
onFavorite,
}) => {
const { updateSidebar } = useContext(SidebarContext)
@ -89,6 +95,7 @@ const PhotoGallery = ({
updateSidebar(<MediaSidebar media={photo} />)
onSelectImage(index)
}}
onFavorite={onFavorite}
setPresenting={setPresenting}
minWidth={minWidth}
index={index}
@ -105,8 +112,12 @@ const PhotoGallery = ({
return photoElements
}
useEffect(() => {
!loading && forceVisible()
}, [loading])
return (
<div>
<ClearWrap>
<Gallery>
<Loader active={loading}>Loading images</Loader>
{getPhotoElements(updateSidebar)}
@ -118,7 +129,7 @@ const PhotoGallery = ({
{...{ nextImage, previousImage, setPresenting }}
/>
)}
</div>
</ClearWrap>
)
}
@ -131,6 +142,7 @@ PhotoGallery.propTypes = {
setPresenting: PropTypes.func,
nextImage: PropTypes.func,
previousImage: PropTypes.func,
onFavorite: PropTypes.func,
}
export default PhotoGallery