From b5be84bbe4c8cf051c9eaa5ba580bcfb9a763e74 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 3 Oct 2021 15:36:10 +0200 Subject: [PATCH 01/11] Make mapbox map more reusable + add coordinates to exif --- api/graphql/generated.go | 206 +++++++++++++++--- api/graphql/models/generated.go | 7 + api/graphql/models/media_exif.go | 11 + api/graphql/schema.graphql | 11 +- ui/src/Pages/PlacesPage/MapClusterMarker.tsx | 12 - ui/src/Pages/PlacesPage/MapPresentMarker.tsx | 3 + ui/src/Pages/PlacesPage/PlacesPage.tsx | 168 ++++++-------- .../PlacesPage/__generated__/mediaGeoJson.ts | 15 ++ .../PlacesPage/mapboxHelperFunctions.tsx | 80 ------- ui/src/components/mapbox/MapboxMap.tsx | 74 +++++++ .../mapbox/__generated__/mapboxToken.ts | 19 ++ .../mapbox/mapboxHelperFunctions.tsx | 93 ++++++++ 12 files changed, 481 insertions(+), 218 deletions(-) create mode 100644 ui/src/Pages/PlacesPage/__generated__/mediaGeoJson.ts delete mode 100644 ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx create mode 100644 ui/src/components/mapbox/MapboxMap.tsx create mode 100644 ui/src/components/mapbox/__generated__/mapboxToken.ts create mode 100644 ui/src/components/mapbox/mapboxHelperFunctions.tsx diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 910e648..08d80d2 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -75,6 +75,11 @@ type ComplexityRoot struct { Token func(childComplexity int) int } + Coordinates struct { + Latitude func(childComplexity int) int + Longitude func(childComplexity int) int + } + FaceGroup struct { ID func(childComplexity int) int ImageFaceCount func(childComplexity int) int @@ -122,6 +127,7 @@ type ComplexityRoot struct { MediaExif struct { Aperture func(childComplexity int) int Camera func(childComplexity int) int + Coordinates func(childComplexity int) int DateShot func(childComplexity int) int Exposure func(childComplexity int) int ExposureProgram func(childComplexity int) int @@ -473,6 +479,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AuthorizeResult.Token(childComplexity), true + case "Coordinates.latitude": + if e.complexity.Coordinates.Latitude == nil { + break + } + + return e.complexity.Coordinates.Latitude(childComplexity), true + + case "Coordinates.longitude": + if e.complexity.Coordinates.Longitude == nil { + break + } + + return e.complexity.Coordinates.Longitude(childComplexity), true + case "FaceGroup.id": if e.complexity.FaceGroup.ID == nil { break @@ -695,6 +715,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.MediaExif.Camera(childComplexity), true + case "MediaEXIF.coordinates": + if e.complexity.MediaExif.Coordinates == nil { + break + } + + return e.complexity.MediaExif.Coordinates(childComplexity), true + case "MediaEXIF.dateShot": if e.complexity.MediaExif.DateShot == nil { break @@ -1745,7 +1772,7 @@ type Query { "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! @isAuthorized "Get the mapbox api token, returns null if mapbox is not enabled" - mapboxToken: String @isAuthorized + mapboxToken: String shareToken(credentials: ShareTokenCredentials!): ShareToken! shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean! @@ -1955,8 +1982,6 @@ type Album { path: [Album!]! shares: [ShareToken!]! - - #coverID: Int } type MediaURL { @@ -2029,6 +2054,14 @@ type MediaEXIF { flash: Int "An index describing the mode for adjusting the exposure of the image" exposureProgram: Int + coordinates: Coordinates +} + +type Coordinates { + "GPS latitude in degrees" + latitude: Float! + "GPS longitude in degrees" + longitude: Float! } type VideoMetadata { @@ -3459,6 +3492,76 @@ func (ec *executionContext) _AuthorizeResult_token(ctx context.Context, field gr return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _Coordinates_latitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Coordinates", + Field: field, + Args: nil, + IsMethod: false, + 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.Latitude, 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.(float64) + fc.Result = res + return ec.marshalNFloat2float64(ctx, field.Selections, res) +} + +func (ec *executionContext) _Coordinates_longitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Coordinates", + Field: field, + Args: nil, + IsMethod: false, + 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.Longitude, 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.(float64) + fc.Result = res + return ec.marshalNFloat2float64(ctx, field.Selections, res) +} + func (ec *executionContext) _FaceGroup_id(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4853,6 +4956,38 @@ func (ec *executionContext) _MediaEXIF_exposureProgram(ctx context.Context, fiel return ec.marshalOInt2ᚖint64(ctx, field.Selections, res) } +func (ec *executionContext) _MediaEXIF_coordinates(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MediaEXIF", + 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.Coordinates(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.Coordinates) + fc.Result = res + return ec.marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx, field.Selections, res) +} + func (ec *executionContext) _MediaURL_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7283,28 +7418,8 @@ func (ec *executionContext) _Query_mapboxToken(ctx context.Context, field graphq ctx = graphql.WithFieldContext(ctx, fc) 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().MapboxToken(rctx) - } - directive1 := func(ctx context.Context) (interface{}, error) { - if ec.directives.IsAuthorized == nil { - return nil, errors.New("directive isAuthorized is not implemented") - } - return ec.directives.IsAuthorized(ctx, nil, directive0) - } - - tmp, err := directive1(rctx) - if err != nil { - return nil, graphql.ErrorOnPath(ctx, err) - } - if tmp == nil { - return nil, nil - } - if data, ok := tmp.(*string); ok { - return data, nil - } - return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp) + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MapboxToken(rctx) }) if err != nil { ec.Error(ctx, err) @@ -10429,6 +10544,38 @@ func (ec *executionContext) _AuthorizeResult(ctx context.Context, sel ast.Select return out } +var coordinatesImplementors = []string{"Coordinates"} + +func (ec *executionContext) _Coordinates(ctx context.Context, sel ast.SelectionSet, obj *models.Coordinates) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, coordinatesImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Coordinates") + case "latitude": + out.Values[i] = ec._Coordinates_latitude(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "longitude": + out.Values[i] = ec._Coordinates_longitude(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var faceGroupImplementors = []string{"FaceGroup"} func (ec *executionContext) _FaceGroup(ctx context.Context, sel ast.SelectionSet, obj *models.FaceGroup) graphql.Marshaler { @@ -10824,6 +10971,8 @@ func (ec *executionContext) _MediaEXIF(ctx context.Context, sel ast.SelectionSet out.Values[i] = ec._MediaEXIF_flash(ctx, field, obj) case "exposureProgram": out.Values[i] = ec._MediaEXIF_exposureProgram(ctx, field, obj) + case "coordinates": + out.Values[i] = ec._MediaEXIF_coordinates(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -12875,6 +13024,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return graphql.MarshalBoolean(*v) } +func (ec *executionContext) marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx context.Context, sel ast.SelectionSet, v *models.Coordinates) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Coordinates(ctx, sel, v) +} + func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v interface{}) (*float64, error) { if v == nil { return nil, nil diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go index 84eaf07..e1922df 100644 --- a/api/graphql/models/generated.go +++ b/api/graphql/models/generated.go @@ -15,6 +15,13 @@ type AuthorizeResult struct { Token *string `json:"token"` } +type Coordinates struct { + // GPS latitude in degrees + Latitude float64 `json:"latitude"` + // GPS longitude in degrees + Longitude float64 `json:"longitude"` +} + type MediaDownload struct { Title string `json:"title"` MediaURL *MediaURL `json:"mediaUrl"` diff --git a/api/graphql/models/media_exif.go b/api/graphql/models/media_exif.go index dec9d67..37af1e4 100644 --- a/api/graphql/models/media_exif.go +++ b/api/graphql/models/media_exif.go @@ -28,3 +28,14 @@ func (MediaEXIF) TableName() string { func (exif *MediaEXIF) Media() *Media { panic("not implemented") } + +func (exif *MediaEXIF) Coordinates() *Coordinates { + if exif.GPSLatitude == nil || exif.GPSLongitude == nil { + return nil + } + + return &Coordinates{ + Latitude: *exif.GPSLatitude, + Longitude: *exif.GPSLongitude, + } +} diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 166b435..36a2df7 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -76,7 +76,7 @@ type Query { "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! @isAuthorized "Get the mapbox api token, returns null if mapbox is not enabled" - mapboxToken: String @isAuthorized + mapboxToken: String shareToken(credentials: ShareTokenCredentials!): ShareToken! shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean! @@ -358,6 +358,15 @@ type MediaEXIF { flash: Int "An index describing the mode for adjusting the exposure of the image" exposureProgram: Int + "GPS coordinates of where the image was taken" + coordinates: Coordinates +} + +type Coordinates { + "GPS latitude in degrees" + latitude: Float! + "GPS longitude in degrees" + longitude: Float! } type VideoMetadata { diff --git a/ui/src/Pages/PlacesPage/MapClusterMarker.tsx b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx index 47d0eb8..4ec4423 100644 --- a/ui/src/Pages/PlacesPage/MapClusterMarker.tsx +++ b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx @@ -52,14 +52,6 @@ const MapClusterMarker = ({ const thumbnail = JSON.parse(marker.thumbnail) const presentMedia = () => { - // presentMarkerClicked({ - // dispatchMedia: dispatchMarkerMedia, - // mediaState: markerMediaState, - // marker: { - // cluster: !!marker.cluster, - // id: marker.cluster ? marker.cluster_id : marker.media_id, - // }, - // }) dispatchMarkerMedia({ type: 'replacePresentMarker', marker: { @@ -67,10 +59,6 @@ const MapClusterMarker = ({ id: marker.cluster ? marker.cluster_id : marker.media_id, }, }) - // setPresentMarker({ - // cluster: !!marker.cluster, - // id: marker.cluster ? marker.cluster_id : marker.media_id, - // }) } return ( diff --git a/ui/src/Pages/PlacesPage/MapPresentMarker.tsx b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx index 2ffb15e..c4b4be4 100644 --- a/ui/src/Pages/PlacesPage/MapPresentMarker.tsx +++ b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx @@ -80,6 +80,9 @@ type MapPresetMarkerProps = { dispatchMarkerMedia: React.Dispatch } +/** + * Full-screen present-view that works with PlacesState + */ const MapPresentMarker = ({ map, markerMediaState, diff --git a/ui/src/Pages/PlacesPage/PlacesPage.tsx b/ui/src/Pages/PlacesPage/PlacesPage.tsx index 1d15005..b41f432 100644 --- a/ui/src/Pages/PlacesPage/PlacesPage.tsx +++ b/ui/src/Pages/PlacesPage/PlacesPage.tsx @@ -1,30 +1,24 @@ import { gql, useQuery } from '@apollo/client' import type mapboxgl from 'mapbox-gl' -import React, { useEffect, useReducer, useRef, useState } from 'react' +import React, { useReducer } from 'react' import { Helmet } from 'react-helmet' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import Layout from '../../components/layout/Layout' -import { makeUpdateMarkers } from './mapboxHelperFunctions' -import MapPresentMarker from './MapPresentMarker' +import { registerMediaMarkers } from '../../components/mapbox/mapboxHelperFunctions' +import useMapboxMap from '../../components/mapbox/MapboxMap' import { urlPresentModeSetupHook } from '../../components/photoGallery/photoGalleryReducer' -import { placesReducer } from './placesReducer' - -import 'mapbox-gl/dist/mapbox-gl.css' +import MapPresentMarker from './MapPresentMarker' +import { PlacesAction, placesReducer } from './placesReducer' +import { mediaGeoJson } from './__generated__/mediaGeoJson' const MapWrapper = styled.div` width: 100%; height: calc(100vh - 120px); ` -const MapContainer = styled.div` - width: 100%; - height: 100%; -` - const MAPBOX_DATA_QUERY = gql` - query placePageMapboxToken { - mapboxToken + query mediaGeoJson { myMediaGeoJson } ` @@ -37,10 +31,9 @@ export type PresentMarker = { const MapPage = () => { const { t } = useTranslation() - const [mapboxLibrary, setMapboxLibrary] = useState() - const mapContainer = useRef(null) - const map = useRef(null) - // const [presentMarker, setPresentMarker] = useState(null) + const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY, { + fetchPolicy: 'cache-first', + }) const [markerMediaState, dispatchMarkerMedia] = useReducer(placesReducer, { presenting: false, @@ -48,76 +41,21 @@ const MapPage = () => { media: [], }) - const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY) + const { mapContainer, mapboxMap, mapboxToken } = useMapboxMap({ + configureMapbox: configureMapbox({ mapboxData, dispatchMarkerMedia }), + }) - useEffect(() => { - async function loadMapboxLibrary() { - const mapbox = (await import('mapbox-gl')).default - - setMapboxLibrary(mapbox) - } - loadMapboxLibrary() - }, []) - - useEffect(() => { - if ( - mapboxLibrary == null || - mapContainer.current == null || - mapboxData == null || - map.current != null - ) { - return - } - - mapboxLibrary.accessToken = mapboxData.mapboxToken - - map.current = new mapboxLibrary.Map({ - container: mapContainer.current, - style: 'mapbox://styles/mapbox/streets-v11', - zoom: 1, - }) - - // Add map navigation control - map.current.addControl(new mapboxLibrary.NavigationControl()) - - map.current.on('load', () => { - if (map.current == null) { - console.error('ERROR: map is null') - return - } - - map.current.addSource('media', { - type: 'geojson', - data: mapboxData.myMediaGeoJson, - cluster: true, - clusterRadius: 50, - clusterProperties: { - thumbnail: ['coalesce', ['get', 'thumbnail'], false], - }, + urlPresentModeSetupHook({ + dispatchMedia: dispatchMarkerMedia, + openPresentMode: event => { + dispatchMarkerMedia({ + type: 'openPresentMode', + activeIndex: event.state.activeIndex, }) + }, + }) - // Add dummy layer for features to be queryable - map.current.addLayer({ - id: 'media-points', - type: 'circle', - source: 'media', - filter: ['!', true], - }) - - const updateMarkers = makeUpdateMarkers({ - map: map.current, - mapboxLibrary, - dispatchMarkerMedia, - }) - - map.current.on('move', updateMarkers) - map.current.on('moveend', updateMarkers) - map.current.on('sourcedata', updateMarkers) - updateMarkers() - }) - }, [mapContainer, mapboxLibrary, mapboxData]) - - if (mapboxData && mapboxData.mapboxToken == null) { + if (mapboxData && mapboxToken == null) { return (

Mapbox token is not set

@@ -134,34 +72,64 @@ const MapPage = () => { ) } - urlPresentModeSetupHook({ - dispatchMedia: dispatchMarkerMedia, - openPresentMode: event => { - dispatchMarkerMedia({ - type: 'openPresentMode', - activeIndex: event.state.activeIndex, - }) - }, - }) - return ( {/* */} {/* */} - - - + {mapContainer} ) } +const configureMapbox = + ({ + mapboxData, + dispatchMarkerMedia, + }: { + mapboxData?: mediaGeoJson + dispatchMarkerMedia: React.Dispatch + }) => + (map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl) => { + // Add map navigation control + map.addControl(new mapboxLibrary.NavigationControl()) + + map.on('load', () => { + if (map == null) { + console.error('ERROR: map is null') + return + } + + map.addSource('media', { + type: 'geojson', + data: mapboxData?.myMediaGeoJson, + cluster: true, + clusterRadius: 50, + clusterProperties: { + thumbnail: ['coalesce', ['get', 'thumbnail'], false], + }, + }) + + // Add dummy layer for features to be queryable + map.addLayer({ + id: 'media-points', + type: 'circle', + source: 'media', + filter: ['!', true], + }) + + registerMediaMarkers({ + map: map, + mapboxLibrary, + dispatchMarkerMedia, + }) + }) + } + export default MapPage diff --git a/ui/src/Pages/PlacesPage/__generated__/mediaGeoJson.ts b/ui/src/Pages/PlacesPage/__generated__/mediaGeoJson.ts new file mode 100644 index 0000000..5f6d5da --- /dev/null +++ b/ui/src/Pages/PlacesPage/__generated__/mediaGeoJson.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mediaGeoJson +// ==================================================== + +export interface mediaGeoJson { + /** + * Get media owned by the logged in user, returned in GeoJson format + */ + myMediaGeoJson: any +} diff --git a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx deleted file mode 100644 index dad9147..0000000 --- a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type mapboxgl from 'mapbox-gl' -import type geojson from 'geojson' -import React from 'react' -import ReactDOM from 'react-dom' -import MapClusterMarker from './MapClusterMarker' -import { MediaMarker } from './MapPresentMarker' -import { PlacesAction } from './placesReducer' - -const markers: { [key: string]: mapboxgl.Marker } = {} -let markersOnScreen: typeof markers = {} - -type makeUpdateMarkersArgs = { - map: mapboxgl.Map - mapboxLibrary: typeof mapboxgl - dispatchMarkerMedia: React.Dispatch - // setPresentMarker: React.Dispatch> -} - -export const makeUpdateMarkers = ({ - map, - mapboxLibrary, - dispatchMarkerMedia, -}: makeUpdateMarkersArgs) => () => { - const newMarkers: typeof markers = {} - const features = map.querySourceFeatures('media') - - // for every media on the screen, create an HTML marker for it (if we didn't yet), - // and add it to the map if it's not there already - for (const feature of features) { - const point = feature.geometry as geojson.Point - const coords = point.coordinates as [number, number] - const props = feature.properties as MediaMarker - if (props == null) { - console.warn('WARN: geojson feature had no properties', feature) - continue - } - - const id = props.cluster - ? `cluster_${props.cluster_id}` - : `media_${props.media_id}` - - let marker = markers[id] - if (!marker) { - const el = createClusterPopupElement(props, { - dispatchMarkerMedia, - }) - marker = markers[id] = new mapboxLibrary.Marker({ - element: el, - }).setLngLat(coords) - } - newMarkers[id] = marker - - if (!markersOnScreen[id]) marker.addTo(map) - } - // for every marker we've added previously, remove those that are no longer visible - for (const id in markersOnScreen) { - if (!newMarkers[id]) markersOnScreen[id].remove() - } - markersOnScreen = newMarkers -} - -function createClusterPopupElement( - geojsonProps: MediaMarker, - { - dispatchMarkerMedia, - }: { - dispatchMarkerMedia: React.Dispatch - } -) { - // setPresentMarker: React.Dispatch> - const el = document.createElement('div') - ReactDOM.render( - , - el - ) - return el -} diff --git a/ui/src/components/mapbox/MapboxMap.tsx b/ui/src/components/mapbox/MapboxMap.tsx new file mode 100644 index 0000000..d996e1c --- /dev/null +++ b/ui/src/components/mapbox/MapboxMap.tsx @@ -0,0 +1,74 @@ +import React, { useState, useRef, useEffect } from 'react' +import { gql, useQuery } from '@apollo/client' +import type mapboxgl from 'mapbox-gl' +import styled from 'styled-components' + +import 'mapbox-gl/dist/mapbox-gl.css' +import { mapboxToken } from './__generated__/mapboxToken' + +const MAPBOX_TOKEN_QUERY = gql` + query mapboxToken { + mapboxToken + myMediaGeoJson + } +` + +const MapContainer = styled.div` + width: 100%; + height: 100%; +` + +type MapboxMapProps = { + configureMapbox(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl): void + readonly initialZoom?: number +} + +const useMapboxMap = ({ configureMapbox, initialZoom = 1 }: MapboxMapProps) => { + const [mapboxLibrary, setMapboxLibrary] = useState() + const mapContainer = useRef(null) + const map = useRef(null) + + const { data: mapboxData } = useQuery(MAPBOX_TOKEN_QUERY, { + fetchPolicy: 'cache-first', + }) + + useEffect(() => { + async function loadMapboxLibrary() { + const mapbox = (await import('mapbox-gl')).default + + setMapboxLibrary(mapbox) + } + loadMapboxLibrary() + }, []) + + useEffect(() => { + if ( + mapboxLibrary == null || + mapContainer.current == null || + mapboxData == null || + map.current != null + ) { + return + } + + if (mapboxData.mapboxToken) + mapboxLibrary.accessToken = mapboxData.mapboxToken + + map.current = new mapboxLibrary.Map({ + container: mapContainer.current, + style: 'mapbox://styles/mapbox/streets-v11', + zoom: initialZoom, + }) + + configureMapbox(map.current, mapboxLibrary) + }, [mapContainer, mapboxLibrary, mapboxData]) + + return { + mapContainer: , + mapboxMap: map.current, + mapboxLibrary, + mapboxToken: mapboxData?.mapboxToken || null, + } +} + +export default useMapboxMap diff --git a/ui/src/components/mapbox/__generated__/mapboxToken.ts b/ui/src/components/mapbox/__generated__/mapboxToken.ts new file mode 100644 index 0000000..644bbae --- /dev/null +++ b/ui/src/components/mapbox/__generated__/mapboxToken.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mapboxToken +// ==================================================== + +export interface mapboxToken { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null + /** + * Get media owned by the logged in user, returned in GeoJson format + */ + myMediaGeoJson: any +} diff --git a/ui/src/components/mapbox/mapboxHelperFunctions.tsx b/ui/src/components/mapbox/mapboxHelperFunctions.tsx new file mode 100644 index 0000000..6ea3a20 --- /dev/null +++ b/ui/src/components/mapbox/mapboxHelperFunctions.tsx @@ -0,0 +1,93 @@ +import type mapboxgl from 'mapbox-gl' +import type geojson from 'geojson' +import React from 'react' +import ReactDOM from 'react-dom' +import MapClusterMarker from '../../Pages/PlacesPage/MapClusterMarker' +import { MediaMarker } from '../../Pages/PlacesPage/MapPresentMarker' +import { PlacesAction } from '../../Pages/PlacesPage/placesReducer' + +const markers: { [key: string]: mapboxgl.Marker } = {} +let markersOnScreen: typeof markers = {} + +type registerMediaMarkersArgs = { + map: mapboxgl.Map + mapboxLibrary: typeof mapboxgl + dispatchMarkerMedia: React.Dispatch +} + +/** + * Add appropriate event handlers to the map, to render and update media markers + * Expects the provided mapbox map to contain geojson source of media + */ +export const registerMediaMarkers = (args: registerMediaMarkersArgs) => { + const updateMarkers = makeUpdateMarkers(args) + + args.map.on('move', updateMarkers) + args.map.on('moveend', updateMarkers) + args.map.on('sourcedata', updateMarkers) + updateMarkers() +} + +/** + * Make a function that can be passed to Mapbox to tell it how to render and update the image markers + */ +const makeUpdateMarkers = + ({ map, mapboxLibrary, dispatchMarkerMedia }: registerMediaMarkersArgs) => + () => { + const newMarkers: typeof markers = {} + const features = map.querySourceFeatures('media') + + // for every media on the screen, create an HTML marker for it (if we didn't yet), + // and add it to the map if it's not there already + for (const feature of features) { + const point = feature.geometry as geojson.Point + const coords = point.coordinates as [number, number] + const props = feature.properties as MediaMarker + if (props == null) { + console.warn('WARN: geojson feature had no properties', feature) + continue + } + + const id = props.cluster + ? `cluster_${props.cluster_id}` + : `media_${props.media_id}` + + let marker = markers[id] + if (!marker) { + const el = createClusterPopupElement(props, { + dispatchMarkerMedia, + }) + marker = markers[id] = new mapboxLibrary.Marker({ + element: el, + }).setLngLat(coords) + } + newMarkers[id] = marker + + if (!markersOnScreen[id]) marker.addTo(map) + } + // for every marker we've added previously, remove those that are no longer visible + for (const id in markersOnScreen) { + if (!newMarkers[id]) markersOnScreen[id].remove() + } + markersOnScreen = newMarkers + } + +function createClusterPopupElement( + geojsonProps: MediaMarker, + { + dispatchMarkerMedia, + }: { + dispatchMarkerMedia: React.Dispatch + } +) { + // setPresentMarker: React.Dispatch> + const el = document.createElement('div') + ReactDOM.render( + , + el + ) + return el +} From 57d408cb5292582965e36a8656aca164d8e9f049 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 10 Oct 2021 16:54:02 +0200 Subject: [PATCH 02/11] Add coordinates to exif list --- ui/package.json | 2 +- ui/src/Pages/SharePage/SharePage.tsx | 4 + .../SharePage/__generated__/SharePageToken.ts | 118 +++++++++------- .../__generated__/albumPathQuery.ts | 16 +-- ui/src/components/sidebar/MediaSidebar.tsx | 12 ++ .../sidebar/__generated__/sidebarPhoto.ts | 132 ++++++++++-------- .../timelineGallery/TimelineFilters.tsx | 4 +- .../extractedTranslations/da/translation.json | 2 + .../extractedTranslations/de/translation.json | 2 + .../extractedTranslations/en/translation.json | 2 + .../extractedTranslations/es/translation.json | 2 + .../extractedTranslations/fr/translation.json | 2 + .../extractedTranslations/it/translation.json | 2 + .../extractedTranslations/pl/translation.json | 2 + .../extractedTranslations/ru/translation.json | 2 + .../extractedTranslations/sv/translation.json | 2 + 16 files changed, 187 insertions(+), 119 deletions(-) diff --git a/ui/package.json b/ui/package.json index ab0dbc6..4df4166 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts", + "genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts && prettier --write */**/__generated__/*.ts", "extractTranslations": "i18next -c i18next-parser.config.js", "prepare": "(cd .. && npx husky install)" }, diff --git a/ui/src/Pages/SharePage/SharePage.tsx b/ui/src/Pages/SharePage/SharePage.tsx index e07d4c1..15ab360 100644 --- a/ui/src/Pages/SharePage/SharePage.tsx +++ b/ui/src/Pages/SharePage/SharePage.tsx @@ -59,6 +59,10 @@ export const SHARE_TOKEN_QUERY = gql` focalLength flash exposureProgram + coordinates { + longitude + latitude + } } } } diff --git a/ui/src/Pages/SharePage/__generated__/SharePageToken.ts b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts index 0891fc7..e454c8b 100644 --- a/ui/src/Pages/SharePage/__generated__/SharePageToken.ts +++ b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts @@ -3,172 +3,188 @@ // @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_coordinates { + __typename: 'Coordinates' + /** + * GPS longitude in degrees + */ + longitude: number + /** + * GPS latitude in degrees + */ + latitude: 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 + /** + * GPS coordinates of where the image was taken + */ + coordinates: SharePageToken_shareToken_media_exif_coordinates | 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 } diff --git a/ui/src/components/__generated__/albumPathQuery.ts b/ui/src/components/__generated__/albumPathQuery.ts index 258c586..6d141f5 100644 --- a/ui/src/components/__generated__/albumPathQuery.ts +++ b/ui/src/components/__generated__/albumPathQuery.ts @@ -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 } diff --git a/ui/src/components/sidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar.tsx index 9f0b8a3..f19b3c0 100644 --- a/ui/src/components/sidebar/MediaSidebar.tsx +++ b/ui/src/components/sidebar/MediaSidebar.tsx @@ -72,6 +72,10 @@ const SIDEBAR_MEDIA_QUERY = gql` focalLength flash exposureProgram + coordinates { + latitude + longitude + } } faces { id @@ -170,6 +174,13 @@ export const MetadataInfo = ({ media }: MediaInfoProps) => { exif.exposure = `1/${Math.round(1 / exif.exposure)}` } + const coordinates = media.exif.coordinates + if (!isNil(coordinates)) { + exif.coordinates = `${ + Math.round(coordinates.latitude * 1000000) / 1000000 + }, ${Math.round(coordinates.longitude * 1000000) / 1000000}` + } + const exposurePrograms = exposureProgramsLookup(t) if ( @@ -246,6 +257,7 @@ const exifNameLookup = (t: TranslationFn): { [key: string]: string } => ({ iso: t('sidebar.media.exif.name.iso', 'ISO'), focalLength: t('sidebar.media.exif.name.focal_length', 'Focal length'), flash: t('sidebar.media.exif.name.flash', 'Flash'), + coordinates: t('sidebar.media.exif.name.coordinates', 'Coordinates'), }) // From https://exiftool.org/TagNames/EXIF.html diff --git a/ui/src/components/sidebar/__generated__/sidebarPhoto.ts b/ui/src/components/sidebar/__generated__/sidebarPhoto.ts index 0dbad68..8d99075 100644 --- a/ui/src/components/sidebar/__generated__/sidebarPhoto.ts +++ b/ui/src/components/sidebar/__generated__/sidebarPhoto.ts @@ -3,155 +3,171 @@ // @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_coordinates { + __typename: 'Coordinates' + /** + * GPS latitude in degrees + */ + latitude: number + /** + * GPS longitude in degrees + */ + longitude: number } 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 + /** + * GPS coordinates of where the image was taken + */ + coordinates: sidebarPhoto_media_exif_coordinates | 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 +175,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 } diff --git a/ui/src/components/timelineGallery/TimelineFilters.tsx b/ui/src/components/timelineGallery/TimelineFilters.tsx index 0884a27..1702550 100644 --- a/ui/src/components/timelineGallery/TimelineFilters.tsx +++ b/ui/src/components/timelineGallery/TimelineFilters.tsx @@ -53,7 +53,9 @@ const DateSelector = ({ filterDate, setFilterDate }: DateSelectorProps) => { const yearItems = years.map(x => ({ value: `${x}`, - label: `${x} and earlier`, + label: t('timeline_filter.date.dropdown_year', '{{year}} and earlier', { + year: x, + }), })) items = [...items, ...yearItems] } diff --git a/ui/src/extractedTranslations/da/translation.json b/ui/src/extractedTranslations/da/translation.json index fe49443..81209a0 100644 --- a/ui/src/extractedTranslations/da/translation.json +++ b/ui/src/extractedTranslations/da/translation.json @@ -288,6 +288,7 @@ "name": { "aperture": "Blænde", "camera": "Kamera", + "coordinates": "Koordinater", "date_shot": "Dato", "exposure": "Lukketid", "exposure_program": "Lukketid program", @@ -319,6 +320,7 @@ "timeline_filter": { "date": { "dropdown_all": "Fra i dag", + "dropdown_year": "{{year}} og tidligere", "label": "Dato" } }, diff --git a/ui/src/extractedTranslations/de/translation.json b/ui/src/extractedTranslations/de/translation.json index ee94dd1..3db9459 100644 --- a/ui/src/extractedTranslations/de/translation.json +++ b/ui/src/extractedTranslations/de/translation.json @@ -288,6 +288,7 @@ "name": { "aperture": "Blende", "camera": "Kamera", + "coordinates": "", "date_shot": "Aufnahmedatum", "exposure": "Belichtung", "exposure_program": "Programm", @@ -319,6 +320,7 @@ "timeline_filter": { "date": { "dropdown_all": "", + "dropdown_year": "", "label": "" } }, diff --git a/ui/src/extractedTranslations/en/translation.json b/ui/src/extractedTranslations/en/translation.json index c64f063..d2c759e 100644 --- a/ui/src/extractedTranslations/en/translation.json +++ b/ui/src/extractedTranslations/en/translation.json @@ -288,6 +288,7 @@ "name": { "aperture": "Aperture", "camera": "Camera", + "coordinates": "Coordinates", "date_shot": "Date shot", "exposure": "Exposure", "exposure_program": "Program", @@ -319,6 +320,7 @@ "timeline_filter": { "date": { "dropdown_all": "From today", + "dropdown_year": "{{year}} and earlier", "label": "Date" } }, diff --git a/ui/src/extractedTranslations/es/translation.json b/ui/src/extractedTranslations/es/translation.json index 7d1d0b7..1235728 100644 --- a/ui/src/extractedTranslations/es/translation.json +++ b/ui/src/extractedTranslations/es/translation.json @@ -288,6 +288,7 @@ "name": { "aperture": "Abertura", "camera": "Cámara", + "coordinates": "", "date_shot": "Fecha de captura", "exposure": "Exposición", "exposure_program": "Programa", @@ -319,6 +320,7 @@ "timeline_filter": { "date": { "dropdown_all": "", + "dropdown_year": "", "label": "" } }, diff --git a/ui/src/extractedTranslations/fr/translation.json b/ui/src/extractedTranslations/fr/translation.json index 64eb30c..c5bcfb5 100644 --- a/ui/src/extractedTranslations/fr/translation.json +++ b/ui/src/extractedTranslations/fr/translation.json @@ -288,6 +288,7 @@ "name": { "aperture": "Ouverture", "camera": "Modèle", + "coordinates": "", "date_shot": "Prise de vue", "exposure": "Vitesse", "exposure_program": "Programme", @@ -319,6 +320,7 @@ "timeline_filter": { "date": { "dropdown_all": "Depuis aujourd'hui", + "dropdown_year": "", "label": "Date" } }, diff --git a/ui/src/extractedTranslations/it/translation.json b/ui/src/extractedTranslations/it/translation.json index b64f070..ce6b730 100644 --- a/ui/src/extractedTranslations/it/translation.json +++ b/ui/src/extractedTranslations/it/translation.json @@ -288,6 +288,7 @@ "name": { "aperture": "Apertura", "camera": "Fotocamera", + "coordinates": "", "date_shot": "Data di scatto", "exposure": "Esposizione", "exposure_program": "Programma", @@ -319,6 +320,7 @@ "timeline_filter": { "date": { "dropdown_all": "", + "dropdown_year": "", "label": "" } }, diff --git a/ui/src/extractedTranslations/pl/translation.json b/ui/src/extractedTranslations/pl/translation.json index 962ab26..3f26dd6 100644 --- a/ui/src/extractedTranslations/pl/translation.json +++ b/ui/src/extractedTranslations/pl/translation.json @@ -293,6 +293,7 @@ "name": { "aperture": "Przysłona", "camera": "Aparat ", + "coordinates": "", "date_shot": "Data wykonania", "exposure": "Ekspozycja", "exposure_program": "Program", @@ -324,6 +325,7 @@ "timeline_filter": { "date": { "dropdown_all": "", + "dropdown_year": "", "label": "" } }, diff --git a/ui/src/extractedTranslations/ru/translation.json b/ui/src/extractedTranslations/ru/translation.json index d6b1530..b4bfb79 100644 --- a/ui/src/extractedTranslations/ru/translation.json +++ b/ui/src/extractedTranslations/ru/translation.json @@ -293,6 +293,7 @@ "name": { "aperture": "Диафрагма", "camera": "Камера", + "coordinates": "", "date_shot": "Дата снимка", "exposure": "Экспозиция", "exposure_program": "Программа", @@ -324,6 +325,7 @@ "timeline_filter": { "date": { "dropdown_all": "", + "dropdown_year": "", "label": "" } }, diff --git a/ui/src/extractedTranslations/sv/translation.json b/ui/src/extractedTranslations/sv/translation.json index 19c1aae..c19ff9a 100644 --- a/ui/src/extractedTranslations/sv/translation.json +++ b/ui/src/extractedTranslations/sv/translation.json @@ -288,6 +288,7 @@ "name": { "aperture": "Bländare", "camera": "Kamera", + "coordinates": "", "date_shot": "Datum tagen", "exposure": "Exponering", "exposure_program": "Program", @@ -319,6 +320,7 @@ "timeline_filter": { "date": { "dropdown_all": "", + "dropdown_year": "", "label": "" } }, From c357532613d12394da150aca744f0139d522b178 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 10 Oct 2021 17:42:09 +0200 Subject: [PATCH 03/11] Add mapbox to sidebar --- ui/src/Pages/SharePage/MediaSharePage.tsx | 2 +- .../components/facesOverlay/FacesOverlay.tsx | 2 +- ui/src/components/mapbox/MapboxMap.tsx | 12 +- .../components/photoGallery/PhotoGallery.tsx | 2 +- .../{ => MediaSidebar}/MediaSidebar.test.js | 0 .../sidebar/MediaSidebar/MediaSidebar.tsx | 242 +++++++++++++++++ .../MediaSidebarExif.tsx} | 247 +----------------- .../sidebar/MediaSidebar/MediaSidebarMap.tsx | 56 ++++ .../sidebar/SidebarDownloadMedia.tsx | 2 +- .../timelineGallery/TimelineGroupAlbum.tsx | 2 +- .../extractedTranslations/da/translation.json | 3 + .../extractedTranslations/de/translation.json | 3 + .../extractedTranslations/en/translation.json | 3 + .../extractedTranslations/es/translation.json | 3 + .../extractedTranslations/fr/translation.json | 3 + .../extractedTranslations/it/translation.json | 3 + .../extractedTranslations/pl/translation.json | 3 + .../extractedTranslations/ru/translation.json | 3 + .../extractedTranslations/sv/translation.json | 3 + 19 files changed, 348 insertions(+), 246 deletions(-) rename ui/src/components/sidebar/{ => MediaSidebar}/MediaSidebar.test.js (100%) create mode 100644 ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx rename ui/src/components/sidebar/{MediaSidebar.tsx => MediaSidebar/MediaSidebarExif.tsx} (58%) create mode 100644 ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx diff --git a/ui/src/Pages/SharePage/MediaSharePage.tsx b/ui/src/Pages/SharePage/MediaSharePage.tsx index 366a790..e7a44e2 100644 --- a/ui/src/Pages/SharePage/MediaSharePage.tsx +++ b/ui/src/Pages/SharePage/MediaSharePage.tsx @@ -6,7 +6,7 @@ import { ProtectedVideo, } from '../../components/photoGallery/ProtectedMedia' import { SidebarContext } from '../../components/sidebar/Sidebar' -import MediaSidebar from '../../components/sidebar/MediaSidebar' +import MediaSidebar from '../../components/sidebar/MediaSidebar/MediaSidebar' import { useTranslation } from 'react-i18next' import { SharePageToken_shareToken_media } from './__generated__/SharePageToken' import { MediaType } from '../../__generated__/globalTypes' diff --git a/ui/src/components/facesOverlay/FacesOverlay.tsx b/ui/src/components/facesOverlay/FacesOverlay.tsx index c38ed1d..a8130bb 100644 --- a/ui/src/components/facesOverlay/FacesOverlay.tsx +++ b/ui/src/components/facesOverlay/FacesOverlay.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Link } from 'react-router-dom' import styled from 'styled-components' import { MediaType } from '../../__generated__/globalTypes' -import { MediaSidebarMedia } from '../sidebar/MediaSidebar' +import { MediaSidebarMedia } from '../sidebar/MediaSidebar/MediaSidebar' import { sidebarPhoto_media_faces } from '../sidebar/__generated__/sidebarPhoto' interface FaceBoxStyleProps { diff --git a/ui/src/components/mapbox/MapboxMap.tsx b/ui/src/components/mapbox/MapboxMap.tsx index d996e1c..071bd06 100644 --- a/ui/src/components/mapbox/MapboxMap.tsx +++ b/ui/src/components/mapbox/MapboxMap.tsx @@ -20,10 +20,13 @@ const MapContainer = styled.div` type MapboxMapProps = { configureMapbox(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl): void - readonly initialZoom?: number + mapboxOptions?: Partial } -const useMapboxMap = ({ configureMapbox, initialZoom = 1 }: MapboxMapProps) => { +const useMapboxMap = ({ + configureMapbox, + mapboxOptions = undefined, +}: MapboxMapProps) => { const [mapboxLibrary, setMapboxLibrary] = useState() const mapContainer = useRef(null) const map = useRef(null) @@ -57,12 +60,15 @@ const useMapboxMap = ({ configureMapbox, initialZoom = 1 }: MapboxMapProps) => { map.current = new mapboxLibrary.Map({ container: mapContainer.current, style: 'mapbox://styles/mapbox/streets-v11', - zoom: initialZoom, + ...mapboxOptions, }) configureMapbox(map.current, mapboxLibrary) + map.current?.resize() }, [mapContainer, mapboxLibrary, mapboxData]) + map.current?.resize() + return { mapContainer: , mapboxMap: map.current, diff --git a/ui/src/components/photoGallery/PhotoGallery.tsx b/ui/src/components/photoGallery/PhotoGallery.tsx index ba1f82e..3d4dd25 100644 --- a/ui/src/components/photoGallery/PhotoGallery.tsx +++ b/ui/src/components/photoGallery/PhotoGallery.tsx @@ -13,7 +13,7 @@ import { toggleFavoriteAction, useMarkFavoriteMutation, } from './photoGalleryMutations' -import MediaSidebar from '../sidebar/MediaSidebar' +import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar' import { SidebarContext } from '../sidebar/Sidebar' const Gallery = styled.div` diff --git a/ui/src/components/sidebar/MediaSidebar.test.js b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.js similarity index 100% rename from ui/src/components/sidebar/MediaSidebar.test.js rename to ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.js diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx new file mode 100644 index 0000000..bf4d5e8 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx @@ -0,0 +1,242 @@ +import { gql, useLazyQuery } from '@apollo/client' +import React, { useEffect } from 'react' +import styled from 'styled-components' +import { authToken } from '../../../helpers/authentication' +import { MediaType } from '../../../__generated__/globalTypes' +import { SidebarFacesOverlay } from '../../facesOverlay/FacesOverlay' +import { + ProtectedImage, + ProtectedVideo, + ProtectedVideoProps_Media, +} from '../../photoGallery/ProtectedMedia' +import { SidebarPhotoCover } from '../AlbumCovers' +import { SidebarPhotoShare } from '../Sharing' +import SidebarMediaDownload from '../SidebarDownloadMedia' +import SidebarHeader from '../SidebarHeader' +import { sidebarDownloadQuery_media_downloads } from '../__generated__/sidebarDownloadQuery' +import { + sidebarPhoto, + sidebarPhotoVariables, + sidebarPhoto_media_exif, + sidebarPhoto_media_faces, + sidebarPhoto_media_thumbnail, + sidebarPhoto_media_videoMetadata, +} from '../__generated__/sidebarPhoto' +import ExifDetails from './MediaSidebarExif' +import MediaSidebarMap from './MediaSidebarMap' + +const SIDEBAR_MEDIA_QUERY = gql` + query sidebarPhoto($id: ID!) { + media(id: $id) { + id + title + type + highRes { + url + width + height + } + thumbnail { + url + width + height + } + videoWeb { + url + width + height + } + videoMetadata { + id + width + height + duration + codec + framerate + bitrate + colorProfile + audio + } + exif { + id + camera + maker + lens + dateShot + exposure + aperture + iso + focalLength + flash + exposureProgram + coordinates { + latitude + longitude + } + } + faces { + id + rectangle { + minX + maxX + minY + maxY + } + faceGroup { + id + } + } + } + } +` + +const PreviewImage = styled(ProtectedImage)` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + object-fit: contain; +` + +const PreviewVideo = styled(ProtectedVideo)` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +` + +interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media { + type: MediaType +} + +type PreviewMediaProps = { + media: PreviewMediaPropsMedia + previewImage?: { + url: string + } +} + +const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => { + if (media.type === MediaType.Photo) { + return + } + + if (media.type === MediaType.Video) { + return + } + + return
ERROR: Unknown media type: {media.type}
+} + +type SidebarContentProps = { + media: MediaSidebarMedia + hidePreview?: boolean +} + +const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { + let previewImage = null + if (media.highRes) previewImage = media.highRes + else if (media.thumbnail) previewImage = media.thumbnail + + const imageAspect = + previewImage?.width && previewImage?.height + ? previewImage.height / previewImage.width + : 3 / 2 + + let sidebarMap = null + const mediaCoordinates = media.exif?.coordinates + if (mediaCoordinates) { + sidebarMap = + } + + return ( +
+ +
+ {!hidePreview && ( +
+ + +
+ )} +
+ + {sidebarMap} + + +
+ +
+
+ ) +} + +export interface MediaSidebarMedia { + __typename: 'Media' + id: string + title?: string + type: MediaType + highRes?: null | { + __typename: 'MediaURL' + url: string + width?: number + height?: number + } + thumbnail?: sidebarPhoto_media_thumbnail | null + videoWeb?: null | { + __typename: 'MediaURL' + url: string + width?: number + height?: number + } + videoMetadata?: sidebarPhoto_media_videoMetadata | null + exif?: sidebarPhoto_media_exif | null + faces?: sidebarPhoto_media_faces[] + downloads?: sidebarDownloadQuery_media_downloads[] +} + +type MediaSidebarType = { + media: MediaSidebarMedia + hidePreview?: boolean +} + +const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => { + const [loadMedia, { loading, error, data }] = useLazyQuery< + sidebarPhoto, + sidebarPhotoVariables + >(SIDEBAR_MEDIA_QUERY) + + useEffect(() => { + if (media != null && authToken()) { + loadMedia({ + variables: { + id: media.id, + }, + }) + } + }, [media]) + + if (!media) return null + + if (!authToken()) { + return + } + + if (error) return
{error.message}
+ + if (loading || data == null) { + return + } + + return +} + +export default MediaSidebar diff --git a/ui/src/components/sidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.tsx similarity index 58% rename from ui/src/components/sidebar/MediaSidebar.tsx rename to ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.tsx index f19b3c0..12adeaf 100644 --- a/ui/src/components/sidebar/MediaSidebar.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.tsx @@ -1,147 +1,20 @@ -import React, { useEffect } from 'react' -import { useLazyQuery, gql } from '@apollo/client' -import styled from 'styled-components' -import { authToken } from '../../helpers/authentication' -import { - ProtectedImage, - ProtectedVideo, - ProtectedVideoProps_Media, -} from '../photoGallery/ProtectedMedia' -import { SidebarPhotoShare } from './Sharing' -import SidebarMediaDownload from './SidebarDownloadMedia' -import SidebarItem from './SidebarItem' -import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay' -import { isNil } from '../../helpers/utils' +import React from 'react' import { useTranslation } from 'react-i18next' -import { MediaType } from '../../__generated__/globalTypes' -import { TranslationFn } from '../../localization' -import { - sidebarPhoto, - sidebarPhotoVariables, - sidebarPhoto_media_exif, - sidebarPhoto_media_faces, - sidebarPhoto_media_thumbnail, - sidebarPhoto_media_videoMetadata, -} from './__generated__/sidebarPhoto' - -import { sidebarDownloadQuery_media_downloads } from './__generated__/sidebarDownloadQuery' -import SidebarHeader from './SidebarHeader' -import { SidebarPhotoCover } from './AlbumCovers' - -const SIDEBAR_MEDIA_QUERY = gql` - query sidebarPhoto($id: ID!) { - media(id: $id) { - id - title - type - highRes { - url - width - height - } - thumbnail { - url - width - height - } - videoWeb { - url - width - height - } - videoMetadata { - id - width - height - duration - codec - framerate - bitrate - colorProfile - audio - } - exif { - id - camera - maker - lens - dateShot - exposure - aperture - iso - focalLength - flash - exposureProgram - coordinates { - latitude - longitude - } - } - faces { - id - rectangle { - minX - maxX - minY - maxY - } - faceGroup { - id - } - } - } - } -` - -const PreviewImage = styled(ProtectedImage)` - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - object-fit: contain; -` - -const PreviewVideo = styled(ProtectedVideo)` - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; -` - -interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media { - type: MediaType -} - -type PreviewMediaProps = { - media: PreviewMediaPropsMedia - previewImage?: { - url: string - } -} - -const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => { - if (media.type === MediaType.Photo) { - return - } - - if (media.type === MediaType.Video) { - return - } - - return
ERROR: Unknown media type: {media.type}
-} +import styled from 'styled-components' +import { isNil } from '../../../helpers/utils' +import { TranslationFn } from '../../../localization' +import SidebarItem from '../SidebarItem' +import { MediaSidebarMedia } from './MediaSidebar' const MetadataInfoContainer = styled.div` margin-bottom: 1.5rem; ` -type MediaInfoProps = { +type ExifDetailsProps = { media?: MediaSidebarMedia } -export const MetadataInfo = ({ media }: MediaInfoProps) => { +const ExifDetails = ({ media }: ExifDetailsProps) => { const { t } = useTranslation() let exifItems: JSX.Element[] = [] @@ -343,106 +216,4 @@ const flashLookup = (t: TranslationFn): { [key: number]: string } => { } } -type SidebarContentProps = { - media: MediaSidebarMedia - hidePreview?: boolean -} - -const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { - let previewImage = null - if (media.highRes) previewImage = media.highRes - else if (media.thumbnail) previewImage = media.thumbnail - - const imageAspect = - previewImage?.width && previewImage?.height - ? previewImage.height / previewImage.width - : 3 / 2 - - return ( -
- -
- {!hidePreview && ( -
- - -
- )} -
- - - -
- -
-
- ) -} - -export interface MediaSidebarMedia { - __typename: 'Media' - id: string - title?: string - type: MediaType - highRes?: null | { - __typename: 'MediaURL' - url: string - width?: number - height?: number - } - thumbnail?: sidebarPhoto_media_thumbnail | null - videoWeb?: null | { - __typename: 'MediaURL' - url: string - width?: number - height?: number - } - videoMetadata?: sidebarPhoto_media_videoMetadata | null - exif?: sidebarPhoto_media_exif | null - faces?: sidebarPhoto_media_faces[] - downloads?: sidebarDownloadQuery_media_downloads[] -} - -type MediaSidebarType = { - media: MediaSidebarMedia - hidePreview?: boolean -} - -const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => { - const [loadMedia, { loading, error, data }] = useLazyQuery< - sidebarPhoto, - sidebarPhotoVariables - >(SIDEBAR_MEDIA_QUERY) - - useEffect(() => { - if (media != null && authToken()) { - loadMedia({ - variables: { - id: media.id, - }, - }) - } - }, [media]) - - if (!media) return null - - if (!authToken()) { - return - } - - if (error) return
{error.message}
- - if (loading || data == null) { - return - } - - return -} - -export default MediaSidebar +export default ExifDetails diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx new file mode 100644 index 0000000..5bdb0e3 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { isNil } from '../../../helpers/utils' +import useMapboxMap from '../../mapbox/MapboxMap' +import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' +import { sidebarPhoto_media_exif_coordinates } from '../__generated__/sidebarPhoto' + +type MediaSidebarMapProps = { + coordinates: sidebarPhoto_media_exif_coordinates +} + +const MediaSidebarMap = ({ coordinates }: MediaSidebarMapProps) => { + const { t } = useTranslation() + + const { mapContainer, mapboxToken } = useMapboxMap({ + mapboxOptions: { + interactive: false, + zoom: 12, + center: { + lat: coordinates.latitude, + lng: coordinates.longitude, + }, + }, + configureMapbox: (map, mapboxLibrary) => { + // todo + map.addControl( + new mapboxLibrary.NavigationControl({ showCompass: false }) + ) + + const centerMarker = new mapboxLibrary.Marker({ + color: 'red', + scale: 0.8, + }) + centerMarker.setLngLat({ + lat: coordinates.latitude, + lng: coordinates.longitude, + }) + centerMarker.addTo(map) + }, + }) + + if (isNil(mapboxToken)) { + return null + } + + return ( + + + {t('sidebar.location.title', 'Location')} + +
{mapContainer}
+
+ ) +} + +export default MediaSidebarMap diff --git a/ui/src/components/sidebar/SidebarDownloadMedia.tsx b/ui/src/components/sidebar/SidebarDownloadMedia.tsx index 261027b..01d4f5e 100644 --- a/ui/src/components/sidebar/SidebarDownloadMedia.tsx +++ b/ui/src/components/sidebar/SidebarDownloadMedia.tsx @@ -4,7 +4,7 @@ import { useLazyQuery, gql } from '@apollo/client' import { authToken } from '../../helpers/authentication' import { useTranslation } from 'react-i18next' import { TranslationFn } from '../../localization' -import { MediaSidebarMedia } from './MediaSidebar' +import { MediaSidebarMedia } from './MediaSidebar/MediaSidebar' import { sidebarDownloadQuery, sidebarDownloadQueryVariables, diff --git a/ui/src/components/timelineGallery/TimelineGroupAlbum.tsx b/ui/src/components/timelineGallery/TimelineGroupAlbum.tsx index c8b3743..7310e37 100644 --- a/ui/src/components/timelineGallery/TimelineGroupAlbum.tsx +++ b/ui/src/components/timelineGallery/TimelineGroupAlbum.tsx @@ -6,7 +6,7 @@ import { toggleFavoriteAction, useMarkFavoriteMutation, } from '../photoGallery/photoGalleryMutations' -import MediaSidebar from '../sidebar/MediaSidebar' +import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar' import { SidebarContext } from '../sidebar/Sidebar' import { getActiveTimelineImage, diff --git a/ui/src/extractedTranslations/da/translation.json b/ui/src/extractedTranslations/da/translation.json index 81209a0..dc8dbcd 100644 --- a/ui/src/extractedTranslations/da/translation.json +++ b/ui/src/extractedTranslations/da/translation.json @@ -259,6 +259,9 @@ }, "title": "Download" }, + "location": { + "title": "Lokation" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/de/translation.json b/ui/src/extractedTranslations/de/translation.json index 3db9459..0e0335c 100644 --- a/ui/src/extractedTranslations/de/translation.json +++ b/ui/src/extractedTranslations/de/translation.json @@ -259,6 +259,9 @@ }, "title": "Download" }, + "location": { + "title": "" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/en/translation.json b/ui/src/extractedTranslations/en/translation.json index d2c759e..0feba47 100644 --- a/ui/src/extractedTranslations/en/translation.json +++ b/ui/src/extractedTranslations/en/translation.json @@ -259,6 +259,9 @@ }, "title": "Download" }, + "location": { + "title": "Location" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/es/translation.json b/ui/src/extractedTranslations/es/translation.json index 1235728..289f4ff 100644 --- a/ui/src/extractedTranslations/es/translation.json +++ b/ui/src/extractedTranslations/es/translation.json @@ -259,6 +259,9 @@ }, "title": "Descargar" }, + "location": { + "title": "" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/fr/translation.json b/ui/src/extractedTranslations/fr/translation.json index c5bcfb5..966d4a9 100644 --- a/ui/src/extractedTranslations/fr/translation.json +++ b/ui/src/extractedTranslations/fr/translation.json @@ -259,6 +259,9 @@ }, "title": "Télécharger" }, + "location": { + "title": "" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/it/translation.json b/ui/src/extractedTranslations/it/translation.json index ce6b730..12ca27f 100644 --- a/ui/src/extractedTranslations/it/translation.json +++ b/ui/src/extractedTranslations/it/translation.json @@ -259,6 +259,9 @@ }, "title": "Download" }, + "location": { + "title": "" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/pl/translation.json b/ui/src/extractedTranslations/pl/translation.json index 3f26dd6..1460305 100644 --- a/ui/src/extractedTranslations/pl/translation.json +++ b/ui/src/extractedTranslations/pl/translation.json @@ -264,6 +264,9 @@ }, "title": "Pobierz" }, + "location": { + "title": "" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/ru/translation.json b/ui/src/extractedTranslations/ru/translation.json index b4bfb79..6a2499f 100644 --- a/ui/src/extractedTranslations/ru/translation.json +++ b/ui/src/extractedTranslations/ru/translation.json @@ -264,6 +264,9 @@ }, "title": "Скачать" }, + "location": { + "title": "" + }, "media": { "exif": { "exposure_program": { diff --git a/ui/src/extractedTranslations/sv/translation.json b/ui/src/extractedTranslations/sv/translation.json index c19ff9a..64cfb91 100644 --- a/ui/src/extractedTranslations/sv/translation.json +++ b/ui/src/extractedTranslations/sv/translation.json @@ -259,6 +259,9 @@ }, "title": "Ladda ner" }, + "location": { + "title": "" + }, "media": { "exif": { "exposure_program": { From 749747aa9c3aa2c3b15ba7b2eb4003d105b4a6f3 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 18 Oct 2021 19:40:02 +0200 Subject: [PATCH 04/11] Add album to media sidebar --- .../sidebar/MediaSidebar/MediaSidebar.tsx | 25 +++ .../__generated__/sidebarPhoto.ts | 193 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 ui/src/components/sidebar/MediaSidebar/__generated__/sidebarPhoto.ts diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx index bf4d5e8..48dc293 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx @@ -1,7 +1,9 @@ import { gql, useLazyQuery } from '@apollo/client' import React, { useEffect } from 'react' +import { Link } from 'react-router-dom' import styled from 'styled-components' import { authToken } from '../../../helpers/authentication' +import { isNil } from '../../../helpers/utils' import { MediaType } from '../../../__generated__/globalTypes' import { SidebarFacesOverlay } from '../../facesOverlay/FacesOverlay' import { @@ -24,6 +26,7 @@ import { } from '../__generated__/sidebarPhoto' import ExifDetails from './MediaSidebarExif' import MediaSidebarMap from './MediaSidebarMap' +import { sidebarPhoto_media_album } from './__generated__/sidebarPhoto' const SIDEBAR_MEDIA_QUERY = gql` query sidebarPhoto($id: ID!) { @@ -74,6 +77,10 @@ const SIDEBAR_MEDIA_QUERY = gql` longitude } } + album { + id + title + } faces { id rectangle { @@ -151,6 +158,22 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { sidebarMap = } + let albumLink = null + const mediaAlbum = media.album + if (!isNil(mediaAlbum)) { + albumLink = ( +
+

Album

+ + {mediaAlbum.title} + +
+ ) + } + return (
@@ -169,6 +192,7 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { )}
+ {albumLink} {sidebarMap} @@ -201,6 +225,7 @@ export interface MediaSidebarMedia { exif?: sidebarPhoto_media_exif | null faces?: sidebarPhoto_media_faces[] downloads?: sidebarDownloadQuery_media_downloads[] + album?: sidebarPhoto_media_album } type MediaSidebarType = { diff --git a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarPhoto.ts b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarPhoto.ts new file mode 100644 index 0000000..275fb03 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarPhoto.ts @@ -0,0 +1,193 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from './../../../../__generated__/globalTypes' + +// ==================================================== +// GraphQL query operation: sidebarPhoto +// ==================================================== + +export interface sidebarPhoto_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 sidebarPhoto_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 sidebarPhoto_media_videoWeb { + __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 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 +} + +export interface sidebarPhoto_media_exif_coordinates { + __typename: 'Coordinates' + /** + * GPS latitude in degrees + */ + latitude: number + /** + * GPS longitude in degrees + */ + longitude: number +} + +export interface sidebarPhoto_media_exif { + __typename: 'MediaEXIF' + id: string + /** + * The model name of the camera + */ + camera: string | null + /** + * The maker of the camera + */ + maker: string | null + /** + * The name of the lens + */ + lens: string | null + dateShot: any | null + /** + * The exposure time of the image + */ + exposure: number | null + /** + * The aperature stops of the image + */ + aperture: number | null + /** + * The ISO setting of the image + */ + iso: number | null + /** + * The focal length of the lens, when the image was taken + */ + focalLength: number | null + /** + * A formatted description of the flash settings, when the image was taken + */ + flash: number | null + /** + * An index describing the mode for adjusting the exposure of the image + */ + exposureProgram: number | null + /** + * GPS coordinates of where the image was taken + */ + coordinates: sidebarPhoto_media_exif_coordinates | null +} + +export interface sidebarPhoto_media_album { + __typename: 'Album' + id: string + title: string +} + +export interface sidebarPhoto_media_faces_rectangle { + __typename: 'FaceRectangle' + minX: number + maxX: number + minY: number + maxY: number +} + +export interface sidebarPhoto_media_faces_faceGroup { + __typename: 'FaceGroup' + id: string +} + +export interface sidebarPhoto_media_faces { + __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 + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: sidebarPhoto_media_highRes | null + /** + * URL to display the media in a smaller resolution + */ + 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 + /** + * The album that holds the media + */ + album: sidebarPhoto_media_album + faces: sidebarPhoto_media_faces[] +} + +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 +} + +export interface sidebarPhotoVariables { + id: string +} From ca9bb092f9836428bb9ce437f04b0d422d0992f5 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 18 Oct 2021 22:35:18 +0200 Subject: [PATCH 05/11] Start on media sidebar people section --- .../components/facesOverlay/FacesOverlay.tsx | 4 +- .../components/photoGallery/PhotoGallery.tsx | 4 +- .../sidebar/MediaSidebar/MediaSidebar.tsx | 45 +++-- .../sidebar/MediaSidebar/MediaSidebarMap.tsx | 4 +- .../MediaSidebar/MediaSidebarPeople.tsx | 35 ++++ .../{sidebarPhoto.ts => sidebarMediaQuery.ts} | 51 ++--- ui/src/components/sidebar/Sharing.tsx | 4 +- .../sidebar/__generated__/setAlbumCoverID.ts | 26 --- .../__generated__/sidbarGetAlbumShares.ts | 36 ---- .../__generated__/sidbarGetPhotoShares.ts | 36 ---- .../sidebar/__generated__/sidebarPhoto.ts | 183 ------------------ .../extractedTranslations/da/translation.json | 4 + .../extractedTranslations/de/translation.json | 4 + .../extractedTranslations/en/translation.json | 4 + .../extractedTranslations/es/translation.json | 4 + .../extractedTranslations/fr/translation.json | 4 + .../extractedTranslations/it/translation.json | 4 + .../extractedTranslations/pl/translation.json | 4 + .../extractedTranslations/ru/translation.json | 4 + .../extractedTranslations/sv/translation.json | 4 + 20 files changed, 131 insertions(+), 333 deletions(-) create mode 100644 ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx rename ui/src/components/sidebar/MediaSidebar/__generated__/{sidebarPhoto.ts => sidebarMediaQuery.ts} (70%) delete mode 100644 ui/src/components/sidebar/__generated__/setAlbumCoverID.ts delete mode 100644 ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts delete mode 100644 ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts delete mode 100644 ui/src/components/sidebar/__generated__/sidebarPhoto.ts diff --git a/ui/src/components/facesOverlay/FacesOverlay.tsx b/ui/src/components/facesOverlay/FacesOverlay.tsx index a8130bb..d60066e 100644 --- a/ui/src/components/facesOverlay/FacesOverlay.tsx +++ b/ui/src/components/facesOverlay/FacesOverlay.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom' import styled from 'styled-components' import { MediaType } from '../../__generated__/globalTypes' import { MediaSidebarMedia } from '../sidebar/MediaSidebar/MediaSidebar' -import { sidebarPhoto_media_faces } from '../sidebar/__generated__/sidebarPhoto' +import { sidebarMediaQuery_media_faces } from '../sidebar/MediaSidebar/__generated__/sidebarMediaQuery' interface FaceBoxStyleProps { $minY: number @@ -23,7 +23,7 @@ const FaceBoxStyle = styled(Link)` ` type FaceBoxProps = { - face: sidebarPhoto_media_faces + face: sidebarMediaQuery_media_faces } const FaceBox = ({ face /*media*/ }: FaceBoxProps) => { diff --git a/ui/src/components/photoGallery/PhotoGallery.tsx b/ui/src/components/photoGallery/PhotoGallery.tsx index 3d4dd25..3521f71 100644 --- a/ui/src/components/photoGallery/PhotoGallery.tsx +++ b/ui/src/components/photoGallery/PhotoGallery.tsx @@ -3,7 +3,6 @@ import styled from 'styled-components' import { MediaThumbnail, MediaPlaceholder } from './MediaThumbnail' import PresentView from './presentView/PresentView' import { PresentMediaProps_Media } from './presentView/PresentMedia' -import { sidebarPhoto_media_thumbnail } from '../sidebar/__generated__/sidebarPhoto' import { openPresentModeAction, PhotoGalleryAction, @@ -15,6 +14,7 @@ import { } from './photoGalleryMutations' import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar' import { SidebarContext } from '../sidebar/Sidebar' +import { sidebarMediaQuery_media_thumbnail } from '../sidebar/MediaSidebar/__generated__/sidebarMediaQuery' const Gallery = styled.div` display: flex; @@ -36,7 +36,7 @@ export const PhotoFiller = styled.div` ` export interface PhotoGalleryProps_Media extends PresentMediaProps_Media { - thumbnail: sidebarPhoto_media_thumbnail | null + thumbnail: sidebarMediaQuery_media_thumbnail | null favorite?: boolean } diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx index 48dc293..059e505 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx @@ -1,5 +1,6 @@ import { gql, useLazyQuery } from '@apollo/client' import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import styled from 'styled-components' import { authToken } from '../../../helpers/authentication' @@ -16,20 +17,21 @@ import { SidebarPhotoShare } from '../Sharing' import SidebarMediaDownload from '../SidebarDownloadMedia' import SidebarHeader from '../SidebarHeader' import { sidebarDownloadQuery_media_downloads } from '../__generated__/sidebarDownloadQuery' -import { - sidebarPhoto, - sidebarPhotoVariables, - sidebarPhoto_media_exif, - sidebarPhoto_media_faces, - sidebarPhoto_media_thumbnail, - sidebarPhoto_media_videoMetadata, -} from '../__generated__/sidebarPhoto' import ExifDetails from './MediaSidebarExif' +import MediaSidebarPeople from './MediaSidebarPeople' import MediaSidebarMap from './MediaSidebarMap' -import { sidebarPhoto_media_album } from './__generated__/sidebarPhoto' +import { + sidebarMediaQuery, + sidebarMediaQueryVariables, + sidebarMediaQuery_media_album, + sidebarMediaQuery_media_exif, + sidebarMediaQuery_media_faces, + sidebarMediaQuery_media_thumbnail, + sidebarMediaQuery_media_videoMetadata, +} from './__generated__/sidebarMediaQuery' const SIDEBAR_MEDIA_QUERY = gql` - query sidebarPhoto($id: ID!) { + query sidebarMediaQuery($id: ID!) { media(id: $id) { id title @@ -91,6 +93,7 @@ const SIDEBAR_MEDIA_QUERY = gql` } faceGroup { id + label } } } @@ -143,6 +146,7 @@ type SidebarContentProps = { } const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { + const { t } = useTranslation() let previewImage = null if (media.highRes) previewImage = media.highRes else if (media.thumbnail) previewImage = media.thumbnail @@ -162,8 +166,10 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { const mediaAlbum = media.album if (!isNil(mediaAlbum)) { albumLink = ( -
-

Album

+
+

+ {t('sidebar.media.album', 'Album')} +

{
{albumLink} + {sidebarMap} @@ -214,18 +221,18 @@ export interface MediaSidebarMedia { width?: number height?: number } - thumbnail?: sidebarPhoto_media_thumbnail | null + thumbnail?: sidebarMediaQuery_media_thumbnail | null videoWeb?: null | { __typename: 'MediaURL' url: string width?: number height?: number } - videoMetadata?: sidebarPhoto_media_videoMetadata | null - exif?: sidebarPhoto_media_exif | null - faces?: sidebarPhoto_media_faces[] + videoMetadata?: sidebarMediaQuery_media_videoMetadata | null + exif?: sidebarMediaQuery_media_exif | null + faces?: sidebarMediaQuery_media_faces[] downloads?: sidebarDownloadQuery_media_downloads[] - album?: sidebarPhoto_media_album + album?: sidebarMediaQuery_media_album } type MediaSidebarType = { @@ -235,8 +242,8 @@ type MediaSidebarType = { const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => { const [loadMedia, { loading, error, data }] = useLazyQuery< - sidebarPhoto, - sidebarPhotoVariables + sidebarMediaQuery, + sidebarMediaQueryVariables >(SIDEBAR_MEDIA_QUERY) useEffect(() => { diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx index 5bdb0e3..1fa0f2c 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarMap.tsx @@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next' import { isNil } from '../../../helpers/utils' import useMapboxMap from '../../mapbox/MapboxMap' import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' -import { sidebarPhoto_media_exif_coordinates } from '../__generated__/sidebarPhoto' +import { sidebarMediaQuery_media_exif_coordinates } from './__generated__/sidebarMediaQuery' type MediaSidebarMapProps = { - coordinates: sidebarPhoto_media_exif_coordinates + coordinates: sidebarMediaQuery_media_exif_coordinates } const MediaSidebarMap = ({ coordinates }: MediaSidebarMapProps) => { diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx new file mode 100644 index 0000000..3371b2c --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' +import { MediaSidebarMedia } from './MediaSidebar' +import { sidebarMediaQuery_media_faces } from './__generated__/sidebarMediaQuery' + +type MediaSidebarFaceProps = { + face: sidebarMediaQuery_media_faces +} + +const MediaSidebarPerson = ({ face }: MediaSidebarFaceProps) => { + return
{face.faceGroup.label ?? 'unlabeled'}
+} + +type MediaSidebarFacesProps = { + media: MediaSidebarMedia +} + +const MediaSidebarPeople = ({ media }: MediaSidebarFacesProps) => { + const { t } = useTranslation() + const faceElms = (media.faces ?? []).map(face => ( + + )) + + return ( + + + {t('sidebar.people.title', 'People')} + +
{faceElms}
+
+ ) +} + +export default MediaSidebarPeople diff --git a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarPhoto.ts b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts similarity index 70% rename from ui/src/components/sidebar/MediaSidebar/__generated__/sidebarPhoto.ts rename to ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts index 275fb03..9388239 100644 --- a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarPhoto.ts +++ b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts @@ -6,10 +6,10 @@ import { MediaType } from './../../../../__generated__/globalTypes' // ==================================================== -// GraphQL query operation: sidebarPhoto +// GraphQL query operation: sidebarMediaQuery // ==================================================== -export interface sidebarPhoto_media_highRes { +export interface sidebarMediaQuery_media_highRes { __typename: 'MediaURL' /** * URL for previewing the image @@ -25,7 +25,7 @@ export interface sidebarPhoto_media_highRes { height: number } -export interface sidebarPhoto_media_thumbnail { +export interface sidebarMediaQuery_media_thumbnail { __typename: 'MediaURL' /** * URL for previewing the image @@ -41,7 +41,7 @@ export interface sidebarPhoto_media_thumbnail { height: number } -export interface sidebarPhoto_media_videoWeb { +export interface sidebarMediaQuery_media_videoWeb { __typename: 'MediaURL' /** * URL for previewing the image @@ -57,7 +57,7 @@ export interface sidebarPhoto_media_videoWeb { height: number } -export interface sidebarPhoto_media_videoMetadata { +export interface sidebarMediaQuery_media_videoMetadata { __typename: 'VideoMetadata' id: string width: number @@ -70,7 +70,7 @@ export interface sidebarPhoto_media_videoMetadata { audio: string | null } -export interface sidebarPhoto_media_exif_coordinates { +export interface sidebarMediaQuery_media_exif_coordinates { __typename: 'Coordinates' /** * GPS latitude in degrees @@ -82,7 +82,7 @@ export interface sidebarPhoto_media_exif_coordinates { longitude: number } -export interface sidebarPhoto_media_exif { +export interface sidebarMediaQuery_media_exif { __typename: 'MediaEXIF' id: string /** @@ -125,16 +125,16 @@ export interface sidebarPhoto_media_exif { /** * GPS coordinates of where the image was taken */ - coordinates: sidebarPhoto_media_exif_coordinates | null + coordinates: sidebarMediaQuery_media_exif_coordinates | null } -export interface sidebarPhoto_media_album { +export interface sidebarMediaQuery_media_album { __typename: 'Album' id: string title: string } -export interface sidebarPhoto_media_faces_rectangle { +export interface sidebarMediaQuery_media_faces_rectangle { __typename: 'FaceRectangle' minX: number maxX: number @@ -142,19 +142,20 @@ export interface sidebarPhoto_media_faces_rectangle { maxY: number } -export interface sidebarPhoto_media_faces_faceGroup { +export interface sidebarMediaQuery_media_faces_faceGroup { __typename: 'FaceGroup' id: string + label: string | null } -export interface sidebarPhoto_media_faces { +export interface sidebarMediaQuery_media_faces { __typename: 'ImageFace' id: string - rectangle: sidebarPhoto_media_faces_rectangle - faceGroup: sidebarPhoto_media_faces_faceGroup + rectangle: sidebarMediaQuery_media_faces_rectangle + faceGroup: sidebarMediaQuery_media_faces_faceGroup } -export interface sidebarPhoto_media { +export interface sidebarMediaQuery_media { __typename: 'Media' id: string title: string @@ -162,32 +163,32 @@ export interface sidebarPhoto_media { /** * URL to display the photo in full resolution, will be null for videos */ - highRes: sidebarPhoto_media_highRes | null + highRes: sidebarMediaQuery_media_highRes | null /** * URL to display the media in a smaller resolution */ - thumbnail: sidebarPhoto_media_thumbnail | null + thumbnail: sidebarMediaQuery_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 + videoWeb: sidebarMediaQuery_media_videoWeb | null + videoMetadata: sidebarMediaQuery_media_videoMetadata | null + exif: sidebarMediaQuery_media_exif | null /** * The album that holds the media */ - album: sidebarPhoto_media_album - faces: sidebarPhoto_media_faces[] + album: sidebarMediaQuery_media_album + faces: sidebarMediaQuery_media_faces[] } -export interface sidebarPhoto { +export interface sidebarMediaQuery { /** * 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: sidebarMediaQuery_media } -export interface sidebarPhotoVariables { +export interface sidebarMediaQueryVariables { id: string } diff --git a/ui/src/components/sidebar/Sharing.tsx b/ui/src/components/sidebar/Sharing.tsx index c2ccb80..b282627 100644 --- a/ui/src/components/sidebar/Sharing.tsx +++ b/ui/src/components/sidebar/Sharing.tsx @@ -13,7 +13,6 @@ import { sidebareDeleteShare, sidebareDeleteShareVariables, } from './__generated__/sidebareDeleteShare' -import { sidbarGetPhotoShares_media_shares } from './__generated__/sidbarGetPhotoShares' import { sidebarPhotoAddShare, sidebarPhotoAddShareVariables, @@ -25,6 +24,7 @@ import { import { sidebarGetPhotoShares, sidebarGetPhotoSharesVariables, + sidebarGetPhotoShares_media_shares, } from './__generated__/sidebarGetPhotoShares' import { sidebarGetAlbumShares, @@ -358,7 +358,7 @@ type SidebarShareProps = { id: string isPhoto: boolean loading: boolean - shares?: sidbarGetPhotoShares_media_shares[] + shares?: sidebarGetPhotoShares_media_shares[] shareItem(item: { variables: { id: string } }): void } diff --git a/ui/src/components/sidebar/__generated__/setAlbumCoverID.ts b/ui/src/components/sidebar/__generated__/setAlbumCoverID.ts deleted file mode 100644 index f623c12..0000000 --- a/ui/src/components/sidebar/__generated__/setAlbumCoverID.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -// ==================================================== -// GraphQL mutation operation: setAlbumCoverID -// ==================================================== - -export interface setAlbumCoverID_setAlbumCoverID { - __typename: 'Album' - id: string - coverID: string -} - -export interface setAlbumCoverID { - /** - * Assign a cover image to an album, set coverID to -1 to remove the current one - */ - setAlbumCoverID: setAlbumCoverID_setAlbumCoverID -} - -export interface setAlbumCoverIDVariables { - albumID: string - coverID: string -} diff --git a/ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts b/ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts deleted file mode 100644 index 7e7fa27..0000000 --- a/ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -// ==================================================== -// GraphQL query operation: sidbarGetAlbumShares -// ==================================================== - -export interface sidbarGetAlbumShares_album_shares { - __typename: "ShareToken"; - id: string; - token: string; - /** - * Whether or not a password is needed to access the share - */ - hasPassword: boolean; -} - -export interface sidbarGetAlbumShares_album { - __typename: "Album"; - id: string; - shares: (sidbarGetAlbumShares_album_shares | null)[] | null; -} - -export interface sidbarGetAlbumShares { - /** - * 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: sidbarGetAlbumShares_album; -} - -export interface sidbarGetAlbumSharesVariables { - id: string; -} diff --git a/ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts b/ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts deleted file mode 100644 index 400e27d..0000000 --- a/ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -// ==================================================== -// GraphQL query operation: sidbarGetPhotoShares -// ==================================================== - -export interface sidbarGetPhotoShares_media_shares { - __typename: "ShareToken"; - id: string; - token: string; - /** - * Whether or not a password is needed to access the share - */ - hasPassword: boolean; -} - -export interface sidbarGetPhotoShares_media { - __typename: "Media"; - id: string; - shares: sidbarGetPhotoShares_media_shares[]; -} - -export interface sidbarGetPhotoShares { - /** - * 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: sidbarGetPhotoShares_media; -} - -export interface sidbarGetPhotoSharesVariables { - id: string; -} diff --git a/ui/src/components/sidebar/__generated__/sidebarPhoto.ts b/ui/src/components/sidebar/__generated__/sidebarPhoto.ts deleted file mode 100644 index 8d99075..0000000 --- a/ui/src/components/sidebar/__generated__/sidebarPhoto.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { MediaType } from './../../../__generated__/globalTypes' - -// ==================================================== -// GraphQL query operation: sidebarPhoto -// ==================================================== - -export interface sidebarPhoto_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 sidebarPhoto_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 sidebarPhoto_media_videoWeb { - __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 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 -} - -export interface sidebarPhoto_media_exif_coordinates { - __typename: 'Coordinates' - /** - * GPS latitude in degrees - */ - latitude: number - /** - * GPS longitude in degrees - */ - longitude: number -} - -export interface sidebarPhoto_media_exif { - __typename: 'MediaEXIF' - id: string - /** - * The model name of the camera - */ - camera: string | null - /** - * The maker of the camera - */ - maker: string | null - /** - * The name of the lens - */ - lens: string | null - dateShot: any | null - /** - * The exposure time of the image - */ - exposure: number | null - /** - * The aperature stops of the image - */ - aperture: number | null - /** - * The ISO setting of the image - */ - iso: number | null - /** - * The focal length of the lens, when the image was taken - */ - focalLength: number | null - /** - * A formatted description of the flash settings, when the image was taken - */ - flash: number | null - /** - * An index describing the mode for adjusting the exposure of the image - */ - exposureProgram: number | null - /** - * GPS coordinates of where the image was taken - */ - coordinates: sidebarPhoto_media_exif_coordinates | null -} - -export interface sidebarPhoto_media_faces_rectangle { - __typename: 'FaceRectangle' - minX: number - maxX: number - minY: number - maxY: number -} - -export interface sidebarPhoto_media_faces_faceGroup { - __typename: 'FaceGroup' - id: string -} - -export interface sidebarPhoto_media_faces { - __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 - /** - * URL to display the photo in full resolution, will be null for videos - */ - highRes: sidebarPhoto_media_highRes | null - /** - * URL to display the media in a smaller resolution - */ - 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[] -} - -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 -} - -export interface sidebarPhotoVariables { - id: string -} diff --git a/ui/src/extractedTranslations/da/translation.json b/ui/src/extractedTranslations/da/translation.json index dc8dbcd..d6fd2db 100644 --- a/ui/src/extractedTranslations/da/translation.json +++ b/ui/src/extractedTranslations/da/translation.json @@ -263,6 +263,7 @@ "title": "Lokation" }, "media": { + "album": "Album", "exif": { "exposure_program": { "action_program": "Actionprogram", @@ -303,6 +304,9 @@ } } }, + "people": { + "title": "Personer" + }, "sharing": { "add_share": "Tilføj deling", "copy_link": "Kopier link", diff --git a/ui/src/extractedTranslations/de/translation.json b/ui/src/extractedTranslations/de/translation.json index 0e0335c..368abda 100644 --- a/ui/src/extractedTranslations/de/translation.json +++ b/ui/src/extractedTranslations/de/translation.json @@ -263,6 +263,7 @@ "title": "" }, "media": { + "album": "", "exif": { "exposure_program": { "action_program": "Action (kurze Verschlusszeit)", @@ -303,6 +304,9 @@ } } }, + "people": { + "title": "" + }, "sharing": { "add_share": "Freigabe hinzufügen", "copy_link": "Link kopieren", diff --git a/ui/src/extractedTranslations/en/translation.json b/ui/src/extractedTranslations/en/translation.json index 0feba47..932cfa6 100644 --- a/ui/src/extractedTranslations/en/translation.json +++ b/ui/src/extractedTranslations/en/translation.json @@ -263,6 +263,7 @@ "title": "Location" }, "media": { + "album": "Album", "exif": { "exposure_program": { "action_program": "Action program", @@ -303,6 +304,9 @@ } } }, + "people": { + "title": "People" + }, "sharing": { "add_share": "Add shares", "copy_link": "Copy Link", diff --git a/ui/src/extractedTranslations/es/translation.json b/ui/src/extractedTranslations/es/translation.json index 289f4ff..4ba5391 100644 --- a/ui/src/extractedTranslations/es/translation.json +++ b/ui/src/extractedTranslations/es/translation.json @@ -263,6 +263,7 @@ "title": "" }, "media": { + "album": "", "exif": { "exposure_program": { "action_program": "Programa de acción", @@ -303,6 +304,9 @@ } } }, + "people": { + "title": "" + }, "sharing": { "add_share": "Añadir compartido", "copy_link": "Copiar enlace", diff --git a/ui/src/extractedTranslations/fr/translation.json b/ui/src/extractedTranslations/fr/translation.json index 966d4a9..4e07e73 100644 --- a/ui/src/extractedTranslations/fr/translation.json +++ b/ui/src/extractedTranslations/fr/translation.json @@ -263,6 +263,7 @@ "title": "" }, "media": { + "album": "", "exif": { "exposure_program": { "action_program": "Programme d'action", @@ -303,6 +304,9 @@ } } }, + "people": { + "title": "" + }, "sharing": { "add_share": "Ajouter un partage", "copy_link": "Copier le lien", diff --git a/ui/src/extractedTranslations/it/translation.json b/ui/src/extractedTranslations/it/translation.json index 12ca27f..17d7984 100644 --- a/ui/src/extractedTranslations/it/translation.json +++ b/ui/src/extractedTranslations/it/translation.json @@ -263,6 +263,7 @@ "title": "" }, "media": { + "album": "", "exif": { "exposure_program": { "action_program": "Programma sport", @@ -303,6 +304,9 @@ } } }, + "people": { + "title": "" + }, "sharing": { "add_share": "Aggiungi condivisione", "copy_link": "Copia il link", diff --git a/ui/src/extractedTranslations/pl/translation.json b/ui/src/extractedTranslations/pl/translation.json index 1460305..d15d921 100644 --- a/ui/src/extractedTranslations/pl/translation.json +++ b/ui/src/extractedTranslations/pl/translation.json @@ -268,6 +268,7 @@ "title": "" }, "media": { + "album": "", "exif": { "exposure_program": { "action_program": "Program działania", @@ -308,6 +309,9 @@ } } }, + "people": { + "title": "" + }, "sharing": { "add_share": "Dodaj udział", "copy_link": "Skopiuj link", diff --git a/ui/src/extractedTranslations/ru/translation.json b/ui/src/extractedTranslations/ru/translation.json index 6a2499f..ecfa6c9 100644 --- a/ui/src/extractedTranslations/ru/translation.json +++ b/ui/src/extractedTranslations/ru/translation.json @@ -268,6 +268,7 @@ "title": "" }, "media": { + "album": "", "exif": { "exposure_program": { "action_program": "Действие программа", @@ -308,6 +309,9 @@ } } }, + "people": { + "title": "" + }, "sharing": { "add_share": "Поделится", "copy_link": "Скопировать ссылку", diff --git a/ui/src/extractedTranslations/sv/translation.json b/ui/src/extractedTranslations/sv/translation.json index 64cfb91..bf4eed7 100644 --- a/ui/src/extractedTranslations/sv/translation.json +++ b/ui/src/extractedTranslations/sv/translation.json @@ -263,6 +263,7 @@ "title": "" }, "media": { + "album": "", "exif": { "exposure_program": { "action_program": "Actionprogram", @@ -303,6 +304,9 @@ } } }, + "people": { + "title": "" + }, "sharing": { "add_share": "Dela", "copy_link": "Kopiera länk", From 06fd1664833f016cea9c8994c6c09a748ef45fbb Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Tue, 19 Oct 2021 23:28:23 +0200 Subject: [PATCH 06/11] Sidebar: people section + album path --- api/gqlgen.yml | 2 + api/graphql/generated.go | 30 ++++++++---- api/graphql/models/face_detection.go | 13 +++++ api/graphql/resolvers/faces.go | 8 ++++ ui/package-lock.json | 11 +++++ ui/package.json | 7 +-- ui/src/components/album/AlbumTitle.tsx | 6 ++- .../sidebar/MediaSidebar/MediaSidebar.tsx | 47 ++++++++++++++----- .../MediaSidebar/MediaSidebarPeople.tsx | 33 ++++++++++++- .../__generated__/sidebarMediaQuery.ts | 34 ++++++++++++++ .../MediaSidebar/icons/peopleDotsIcon.svg | 3 ++ ui/src/helpers/utils.ts | 6 +++ ui/src/primitives/form/Input.tsx | 11 ++++- 13 files changed, 182 insertions(+), 29 deletions(-) create mode 100644 ui/src/components/sidebar/MediaSidebar/icons/peopleDotsIcon.svg diff --git a/api/gqlgen.yml b/api/gqlgen.yml index 8d8d6f7..c49c42a 100644 --- a/api/gqlgen.yml +++ b/api/gqlgen.yml @@ -60,6 +60,8 @@ models: fields: faceGroup: resolver: true + media: + resolver: true FaceRectangle: model: github.com/photoview/photoview/api/graphql/models.FaceRectangle SiteInfo: diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 08d80d2..8b3ed2a 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -288,6 +288,8 @@ type FaceGroupResolver interface { ImageFaceCount(ctx context.Context, obj *models.FaceGroup) (int, error) } type ImageFaceResolver interface { + Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error) + FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error) } type MediaResolver interface { @@ -2054,6 +2056,7 @@ type MediaEXIF { flash: Int "An index describing the mode for adjusting the exposure of the image" exposureProgram: Int + "GPS coordinates of where the image was taken" coordinates: Coordinates } @@ -3892,14 +3895,14 @@ func (ec *executionContext) _ImageFace_media(ctx context.Context, field graphql. Object: "ImageFace", 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.Media, nil + return ec.resolvers.ImageFace().Media(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -3911,9 +3914,9 @@ func (ec *executionContext) _ImageFace_media(ctx context.Context, field graphql. } return graphql.Null } - res := resTmp.(models.Media) + res := resTmp.(*models.Media) fc.Result = res - return ec.marshalNMedia2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res) + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res) } func (ec *executionContext) _ImageFace_rectangle(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) { @@ -10692,10 +10695,19 @@ func (ec *executionContext) _ImageFace(ctx context.Context, sel ast.SelectionSet atomic.AddUint32(&invalids, 1) } case "media": - out.Values[i] = ec._ImageFace_media(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._ImageFace_media(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "rectangle": out.Values[i] = ec._ImageFace_rectangle(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/api/graphql/models/face_detection.go b/api/graphql/models/face_detection.go index dca422f..4c91c35 100644 --- a/api/graphql/models/face_detection.go +++ b/api/graphql/models/face_detection.go @@ -31,6 +31,19 @@ type ImageFace struct { Rectangle FaceRectangle `gorm:"not null"` } +func (f *ImageFace) FillMedia(db *gorm.DB) error { + if f.Media.ID != 0 { + // media already exists + return nil + } + + if err := db.Model(&f).Association("Media").Find(&f.Media); err != nil { + return err + } + + return nil +} + type FaceDescriptor face.Descriptor // GormDataType datatype used in database diff --git a/api/graphql/resolvers/faces.go b/api/graphql/resolvers/faces.go index 34fe152..2be354d 100644 --- a/api/graphql/resolvers/faces.go +++ b/api/graphql/resolvers/faces.go @@ -46,6 +46,14 @@ func (r imageFaceResolver) FaceGroup(ctx context.Context, obj *models.ImageFace) return &faceGroup, nil } +func (r imageFaceResolver) Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error) { + if err := obj.FillMedia(r.Database); err != nil { + return nil, err + } + + return &obj.Media, nil +} + func (r faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup, paginate *models.Pagination) ([]*models.ImageFace, error) { user := auth.UserFromContext(ctx) if user == nil { diff --git a/ui/package-lock.json b/ui/package-lock.json index dd55fed..bb88d29 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -49,6 +49,7 @@ "react-test-renderer": "^17.0.2", "styled-components": "^5.3.0", "subscriptions-transport-ws": "^0.9.19", + "tailwind-override": "^0.2.3", "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4", "typescript": "^4.3.5", "url-join": "^4.0.1" @@ -24041,6 +24042,11 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/tailwind-override": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tailwind-override/-/tailwind-override-0.2.3.tgz", + "integrity": "sha512-psWRqXL3TiI2h/YtzRq7dwKO6N7CrsEs4v99rNHgqEclfx4IioM0cHZ9O6pzerV3E6bZi6DhCbeq0z67Xs5PIQ==" + }, "node_modules/tailwindcss": { "name": "@tailwindcss/postcss7-compat", "version": "2.2.4", @@ -46037,6 +46043,11 @@ } } }, + "tailwind-override": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tailwind-override/-/tailwind-override-0.2.3.tgz", + "integrity": "sha512-psWRqXL3TiI2h/YtzRq7dwKO6N7CrsEs4v99rNHgqEclfx4IioM0cHZ9O6pzerV3E6bZi6DhCbeq0z67Xs5PIQ==" + }, "tailwindcss": { "version": "npm:@tailwindcss/postcss7-compat@2.2.4", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.4.tgz", diff --git a/ui/package.json b/ui/package.json index 4df4166..7d7a44e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -49,6 +49,7 @@ "react-test-renderer": "^17.0.2", "styled-components": "^5.3.0", "subscriptions-transport-ws": "^0.9.19", + "tailwind-override": "^0.2.3", "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4", "typescript": "^4.3.5", "url-join": "^4.0.1" @@ -71,12 +72,12 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.1.9", + "apollo": "2.33.4", + "apollo-language-server": "1.26.3", "husky": "^6.0.0", "i18next-parser": "^4.2.0", "lint-staged": "^11.0.1", - "tsc-files": "^1.1.2", - "apollo": "2.33.4", - "apollo-language-server": "1.26.3" + "tsc-files": "^1.1.2" }, "prettier": { "trailingComma": "es5", diff --git a/ui/src/components/album/AlbumTitle.tsx b/ui/src/components/album/AlbumTitle.tsx index f5968be..189deb9 100644 --- a/ui/src/components/album/AlbumTitle.tsx +++ b/ui/src/components/album/AlbumTitle.tsx @@ -10,8 +10,10 @@ import useDelay from '../../hooks/useDelay' import { ReactComponent as GearIcon } from './icons/gear.svg' -const BreadcrumbList = styled.ol` - & li::after { +export const BreadcrumbList = styled.ol<{ hideLastArrow?: boolean }>` + & + ${({ hideLastArrow }) => + hideLastArrow ? 'li:not(:last-child)::after' : 'li::after'} { content: ''; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='5px' height='6px' viewBox='0 0 5 6'%3E%3Cpolyline fill='none' stroke='%23979797' points='0.74 0.167710644 3.57228936 3 0.74 5.83228936' /%3E%3C/svg%3E"); width: 5px; diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx index 059e505..1830177 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx @@ -23,12 +23,13 @@ import MediaSidebarMap from './MediaSidebarMap' import { sidebarMediaQuery, sidebarMediaQueryVariables, - sidebarMediaQuery_media_album, + sidebarMediaQuery_media_album_path, sidebarMediaQuery_media_exif, sidebarMediaQuery_media_faces, sidebarMediaQuery_media_thumbnail, sidebarMediaQuery_media_videoMetadata, } from './__generated__/sidebarMediaQuery' +import { BreadcrumbList } from '../../album/AlbumTitle' const SIDEBAR_MEDIA_QUERY = gql` query sidebarMediaQuery($id: ID!) { @@ -82,6 +83,10 @@ const SIDEBAR_MEDIA_QUERY = gql` album { id title + path { + id + title + } } faces { id @@ -95,6 +100,15 @@ const SIDEBAR_MEDIA_QUERY = gql` id label } + media { + id + title + thumbnail { + url + width + height + } + } } } } @@ -162,20 +176,26 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { sidebarMap = } - let albumLink = null + let albumPath = null const mediaAlbum = media.album if (!isNil(mediaAlbum)) { - albumLink = ( -
-

- {t('sidebar.media.album', 'Album')} -

+ const pathElms = [...(mediaAlbum.path ?? []), mediaAlbum].map(album => ( +
  • - {mediaAlbum.title} + {album.title} +
  • + )) + + albumPath = ( +
    +

    + {t('sidebar.media.album_path', 'Album path')} +

    + {pathElms}
    ) } @@ -198,7 +218,7 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => { )}
    - {albumLink} + {albumPath} {sidebarMap} @@ -232,7 +252,12 @@ export interface MediaSidebarMedia { exif?: sidebarMediaQuery_media_exif | null faces?: sidebarMediaQuery_media_faces[] downloads?: sidebarDownloadQuery_media_downloads[] - album?: sidebarMediaQuery_media_album + album?: { + __typename: 'Album' + id: string + title: string + path?: sidebarMediaQuery_media_album_path[] + } } type MediaSidebarType = { diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx index 3371b2c..49188ac 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx @@ -1,15 +1,39 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import FaceCircleImage from '../../../Pages/PeoplePage/FaceCircleImage' +import { Button } from '../../../primitives/form/Input' import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' import { MediaSidebarMedia } from './MediaSidebar' import { sidebarMediaQuery_media_faces } from './__generated__/sidebarMediaQuery' +import { ReactComponent as PeopleDotsIcon } from './icons/peopleDotsIcon.svg' + type MediaSidebarFaceProps = { face: sidebarMediaQuery_media_faces } const MediaSidebarPerson = ({ face }: MediaSidebarFaceProps) => { - return
    {face.faceGroup.label ?? 'unlabeled'}
    + const { t } = useTranslation() + + return ( +
  • + + + +
    + {face.faceGroup.label ?? + t('people_page.face_group.unlabeled', 'Unlabeled')} + +
    +
  • + ) } type MediaSidebarFacesProps = { @@ -18,16 +42,21 @@ type MediaSidebarFacesProps = { const MediaSidebarPeople = ({ media }: MediaSidebarFacesProps) => { const { t } = useTranslation() + const faceElms = (media.faces ?? []).map(face => ( )) + if (faceElms.length == 0) return null + return ( {t('sidebar.people.title', 'People')} -
    {faceElms}
    +
    +
      {faceElms}
    +
    ) } diff --git a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts index 9388239..01b6199 100644 --- a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts +++ b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts @@ -128,10 +128,17 @@ export interface sidebarMediaQuery_media_exif { coordinates: sidebarMediaQuery_media_exif_coordinates | null } +export interface sidebarMediaQuery_media_album_path { + __typename: 'Album' + id: string + title: string +} + export interface sidebarMediaQuery_media_album { __typename: 'Album' id: string title: string + path: sidebarMediaQuery_media_album_path[] } export interface sidebarMediaQuery_media_faces_rectangle { @@ -148,11 +155,38 @@ export interface sidebarMediaQuery_media_faces_faceGroup { label: string | null } +export interface sidebarMediaQuery_media_faces_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 sidebarMediaQuery_media_faces_media { + __typename: 'Media' + id: string + title: string + /** + * URL to display the media in a smaller resolution + */ + thumbnail: sidebarMediaQuery_media_faces_media_thumbnail | null +} + export interface sidebarMediaQuery_media_faces { __typename: 'ImageFace' id: string rectangle: sidebarMediaQuery_media_faces_rectangle faceGroup: sidebarMediaQuery_media_faces_faceGroup + media: sidebarMediaQuery_media_faces_media } export interface sidebarMediaQuery_media { diff --git a/ui/src/components/sidebar/MediaSidebar/icons/peopleDotsIcon.svg b/ui/src/components/sidebar/MediaSidebar/icons/peopleDotsIcon.svg new file mode 100644 index 0000000..9e8e5ba --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/icons/peopleDotsIcon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index 8879ea3..6f9042e 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -1,3 +1,5 @@ +import classNames, { Argument as ClassNamesArg } from 'classnames' +import { overrideTailwindClasses } from 'tailwind-override' /* eslint-disable @typescript-eslint/no-explicit-any */ export interface DebouncedFn any> { @@ -41,3 +43,7 @@ export function isNil(value: any): value is undefined | null { export function exhaustiveCheck(value: never) { throw new Error(`Exhaustive check failed with value: ${value}`) } + +export function tailwindClassNames(...args: ClassNamesArg[]) { + return overrideTailwindClasses(classNames(args)) +} diff --git a/ui/src/primitives/form/Input.tsx b/ui/src/primitives/form/Input.tsx index bf9dcd2..2fa6056 100644 --- a/ui/src/primitives/form/Input.tsx +++ b/ui/src/primitives/form/Input.tsx @@ -3,6 +3,7 @@ import classNames, { Argument as ClassNamesArg } from 'classnames' import { ReactComponent as ActionArrowIcon } from './icons/textboxActionArrow.svg' import { ReactComponent as LoadingSpinnerIcon } from './icons/textboxLoadingSpinner.svg' import styled from 'styled-components' +import { tailwindClassNames } from '../../helpers/utils' type TextFieldProps = { label?: string @@ -164,7 +165,10 @@ export const Submit = ({ ...props }: SubmitProps & React.ButtonHTMLAttributes) => ( ) => ( +
  • - +
  • - +
  • - +
  • diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx index 49188ac..e4be0f7 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx @@ -2,12 +2,76 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import FaceCircleImage from '../../../Pages/PeoplePage/FaceCircleImage' -import { Button } from '../../../primitives/form/Input' import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' import { MediaSidebarMedia } from './MediaSidebar' import { sidebarMediaQuery_media_faces } from './__generated__/sidebarMediaQuery' import { ReactComponent as PeopleDotsIcon } from './icons/peopleDotsIcon.svg' +import { Menu } from '@headlessui/react' +import { Button } from '../../../primitives/form/Input' +import { ArrowPopoverPanel } from '../Sharing' +import { tailwindClassNames } from '../../../helpers/utils' + +type PersonMoreMenuItemProps = { + label: string + className?: string +} + +const PersonMoreMenuItem = ({ label, className }: PersonMoreMenuItemProps) => { + return ( + + {({ active }) => ( + + {label} + + )} + + ) +} + +type PersonMoreMenuProps = { + face: sidebarMediaQuery_media_faces +} + +const PersonMoreMenu = ({ face }: PersonMoreMenuProps) => { + const { t } = useTranslation() + + face + return ( + + + + + + + + + + + + + + ) +} type MediaSidebarFaceProps = { face: sidebarMediaQuery_media_faces @@ -19,18 +83,16 @@ const MediaSidebarPerson = ({ face }: MediaSidebarFaceProps) => { return (
  • - +
    {face.faceGroup.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')} - +
  • ) @@ -54,8 +116,12 @@ const MediaSidebarPeople = ({ media }: MediaSidebarFacesProps) => { {t('sidebar.people.title', 'People')} -
    +
      {faceElms}
    +
    ) diff --git a/ui/src/components/sidebar/Sharing.tsx b/ui/src/components/sidebar/Sharing.tsx index b282627..e0100f3 100644 --- a/ui/src/components/sidebar/Sharing.tsx +++ b/ui/src/components/sidebar/Sharing.tsx @@ -106,10 +106,12 @@ const DELETE_SHARE_MUTATION = gql` } ` -const ArrowPopoverPanel = styled.div.attrs({ +export const ArrowPopoverPanel = styled.div.attrs({ className: - 'absolute right-6 -top-3 bg-white rounded shadow-md border border-gray-200 w-[260px]', -})` + 'absolute right-6 -top-3 bg-white rounded shadow-md border border-gray-200', +})<{ width: number }>` + width: ${({ width }) => width}px; + &::after { content: ''; position: absolute; @@ -247,7 +249,7 @@ const MorePopover = ({ id, share, query }: MorePopoverProps) => { - +
    diff --git a/ui/src/extractedTranslations/da/translation.json b/ui/src/extractedTranslations/da/translation.json index d6fd2db..525e3c0 100644 --- a/ui/src/extractedTranslations/da/translation.json +++ b/ui/src/extractedTranslations/da/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "Ændre navn", - "detach_face": "Løsriv billeder", - "merge_face": "Sammenflet personer", + "detach_images": "Løsriv billeder", + "merge_people": "Sammenflet personer", "move_faces": "Flyt ansigter" }, "face_group": { @@ -263,7 +263,7 @@ "title": "Lokation" }, "media": { - "album": "Album", + "album_path": "", "exif": { "exposure_program": { "action_program": "Actionprogram", @@ -305,6 +305,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "Personer" }, "sharing": { diff --git a/ui/src/extractedTranslations/da/translation_old.json b/ui/src/extractedTranslations/da/translation_old.json index a888751..ec51c67 100644 --- a/ui/src/extractedTranslations/da/translation_old.json +++ b/ui/src/extractedTranslations/da/translation_old.json @@ -42,6 +42,15 @@ "merge_face": null, "detach_face": null, "move_faces": null + }, + "modal": { + "merge_people_groups": { + "description": "Alle billeder fra denne gruppe vil blive flettet sammen med den valgte gruppe", + "destination_table": { + "title": "Vælg destinationsgruppe" + }, + "title": "Vælg gruppe at flette med" + } } }, "settings": { @@ -71,6 +80,9 @@ "mega_byte_plural": null, "tera_byte_plural": null } + }, + "media": { + "album": "Album" } }, "timeline_filter": { diff --git a/ui/src/extractedTranslations/de/translation.json b/ui/src/extractedTranslations/de/translation.json index 368abda..1fc27d8 100644 --- a/ui/src/extractedTranslations/de/translation.json +++ b/ui/src/extractedTranslations/de/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "", - "detach_face": "", - "merge_face": "", + "detach_images": "", + "merge_people": "", "move_faces": "" }, "face_group": { @@ -263,7 +263,7 @@ "title": "" }, "media": { - "album": "", + "album_path": "", "exif": { "exposure_program": { "action_program": "Action (kurze Verschlusszeit)", @@ -305,6 +305,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/de/translation_old.json b/ui/src/extractedTranslations/de/translation_old.json index aa43241..d21d24f 100644 --- a/ui/src/extractedTranslations/de/translation_old.json +++ b/ui/src/extractedTranslations/de/translation_old.json @@ -61,6 +61,13 @@ "title": null }, "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" } }, "table": { @@ -115,6 +122,9 @@ "mega_byte_plural": null, "tera_byte_plural": null } + }, + "media": { + "album": "" } }, "album_filter": { diff --git a/ui/src/extractedTranslations/en/translation.json b/ui/src/extractedTranslations/en/translation.json index 932cfa6..fabeafe 100644 --- a/ui/src/extractedTranslations/en/translation.json +++ b/ui/src/extractedTranslations/en/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "Change label", - "detach_face": "Detach face", - "merge_face": "Merge face", + "detach_images": "Detach face", + "merge_people": "Merge face", "move_faces": "Move faces" }, "face_group": { @@ -263,7 +263,7 @@ "title": "Location" }, "media": { - "album": "Album", + "album_path": "Album path", "exif": { "exposure_program": { "action_program": "Action program", @@ -305,6 +305,11 @@ } }, "people": { + "action_label": { + "detach_image": "Detach image", + "merge_face": "Merge face", + "move_face": "Move face" + }, "title": "People" }, "sharing": { diff --git a/ui/src/extractedTranslations/en/translation_old.json b/ui/src/extractedTranslations/en/translation_old.json index fecc4be..1a50360 100644 --- a/ui/src/extractedTranslations/en/translation_old.json +++ b/ui/src/extractedTranslations/en/translation_old.json @@ -36,6 +36,15 @@ "select_image_faces": { "search_images_placeholder": "Search images..." } + }, + "modal": { + "merge_people_groups": { + "description": "All images within this face group will be merged into the selected face group.", + "destination_table": { + "title": "Select the destination face" + }, + "title": "Merge Face Groups" + } } }, "settings": { @@ -54,6 +63,9 @@ }, "sharing": { "table_header": "Public shares" + }, + "media": { + "album": "Album" } } } diff --git a/ui/src/extractedTranslations/es/translation.json b/ui/src/extractedTranslations/es/translation.json index 4ba5391..983e1c7 100644 --- a/ui/src/extractedTranslations/es/translation.json +++ b/ui/src/extractedTranslations/es/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "", - "detach_face": "", - "merge_face": "", + "detach_images": "", + "merge_people": "", "move_faces": "" }, "face_group": { @@ -263,7 +263,7 @@ "title": "" }, "media": { - "album": "", + "album_path": "", "exif": { "exposure_program": { "action_program": "Programa de acción", @@ -305,6 +305,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/es/translation_old.json b/ui/src/extractedTranslations/es/translation_old.json index 94a8bb3..eee880e 100644 --- a/ui/src/extractedTranslations/es/translation_old.json +++ b/ui/src/extractedTranslations/es/translation_old.json @@ -61,6 +61,13 @@ "title": null }, "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" } }, "table": { @@ -120,6 +127,9 @@ "mega_byte_plural": null, "tera_byte_plural": null } + }, + "media": { + "album": "" } }, "title": { diff --git a/ui/src/extractedTranslations/fr/translation.json b/ui/src/extractedTranslations/fr/translation.json index 4e07e73..d3fe60d 100644 --- a/ui/src/extractedTranslations/fr/translation.json +++ b/ui/src/extractedTranslations/fr/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "Changer le label", - "detach_face": "Détacher le visage", - "merge_face": "Fusionner le visage", + "detach_images": "Détacher le visage", + "merge_people": "Fusionner le visage", "move_faces": "Déplacer les visages" }, "face_group": { @@ -263,7 +263,7 @@ "title": "" }, "media": { - "album": "", + "album_path": "", "exif": { "exposure_program": { "action_program": "Programme d'action", @@ -305,6 +305,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/fr/translation_old.json b/ui/src/extractedTranslations/fr/translation_old.json index 61e3c86..58420ff 100644 --- a/ui/src/extractedTranslations/fr/translation_old.json +++ b/ui/src/extractedTranslations/fr/translation_old.json @@ -60,6 +60,13 @@ "title": null }, "title": null + }, + "merge_people_groups": { + "description": "Toutes les images de ce groupe de visages seront fusionnées dans le groupe de visages sélectionné.", + "destination_table": { + "title": "Sélectionner le visage de destination" + }, + "title": "Fusionner des groupes de visages" } }, "table": { @@ -95,6 +102,9 @@ }, "sharing": { "table_header": "Partages publics" + }, + "media": { + "album": "" } }, "title": { diff --git a/ui/src/extractedTranslations/it/translation.json b/ui/src/extractedTranslations/it/translation.json index 17d7984..1f942bf 100644 --- a/ui/src/extractedTranslations/it/translation.json +++ b/ui/src/extractedTranslations/it/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "", - "detach_face": "", - "merge_face": "", + "detach_images": "", + "merge_people": "", "move_faces": "" }, "face_group": { @@ -263,7 +263,7 @@ "title": "" }, "media": { - "album": "", + "album_path": "", "exif": { "exposure_program": { "action_program": "Programma sport", @@ -305,6 +305,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/it/translation_old.json b/ui/src/extractedTranslations/it/translation_old.json index b09cdcc..3cd12dd 100644 --- a/ui/src/extractedTranslations/it/translation_old.json +++ b/ui/src/extractedTranslations/it/translation_old.json @@ -49,6 +49,15 @@ }, "tableselect_image_faces": { "search_images_placeholder": null + }, + "modal": { + "merge_people_groups": { + "description": "Tutte le immagini di questo volto saranno unite alle immagini del volto selezionato", + "destination_table": { + "title": "Seleziona il volto di destinazione" + }, + "title": "Unisci immagini volto" + } } }, "settings": { @@ -82,6 +91,9 @@ "mega_byte_plural": null, "tera_byte_plural": null } + }, + "media": { + "album": "" } }, "album_filter": { diff --git a/ui/src/extractedTranslations/pl/translation.json b/ui/src/extractedTranslations/pl/translation.json index d15d921..5fcffc3 100644 --- a/ui/src/extractedTranslations/pl/translation.json +++ b/ui/src/extractedTranslations/pl/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "", - "detach_face": "", - "merge_face": "", + "detach_images": "", + "merge_people": "", "move_faces": "" }, "face_group": { @@ -268,7 +268,7 @@ "title": "" }, "media": { - "album": "", + "album_path": "", "exif": { "exposure_program": { "action_program": "Program działania", @@ -310,6 +310,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/pl/translation_old.json b/ui/src/extractedTranslations/pl/translation_old.json index 19305fd..fab4374 100644 --- a/ui/src/extractedTranslations/pl/translation_old.json +++ b/ui/src/extractedTranslations/pl/translation_old.json @@ -61,6 +61,13 @@ "title": null }, "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" } }, "table": { @@ -137,6 +144,9 @@ "table_header": "Publiczne udostępnienia", "delete": null, "more": null + }, + "media": { + "album": "" } }, "title": { diff --git a/ui/src/extractedTranslations/pt/translation.json b/ui/src/extractedTranslations/pt/translation.json index 358d159..c9c5e16 100644 --- a/ui/src/extractedTranslations/pt/translation.json +++ b/ui/src/extractedTranslations/pt/translation.json @@ -61,17 +61,17 @@ "description": "Galeria de fotos simples e fácil de usar para servidores pessoais" }, "people_page": { + "action_label": { + "change_label": "", + "detach_images": "", + "merge_people": "", + "move_faces": "" + }, "face_group": { "label_placeholder": "Etiqueta", "unlabeled": "Sem etiqueta", "unlabeled_person": "Pessoa sem etiqueta" }, - "action_label": { - "change_label": null, - "merge_face": null, - "detach_face": null, - "move_faces": null - }, "modal": { "action": { "merge": "Juntar" @@ -215,6 +215,27 @@ }, "sidebar": { "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", "title_placeholder": "Título do Álbum" }, "download": { @@ -238,7 +259,11 @@ }, "title": "Descarregar" }, + "location": { + "title": "" + }, "media": { + "album_path": "", "exif": { "exposure_program": { "action_program": "Programa de ação", @@ -267,6 +292,7 @@ "name": { "aperture": "Abertura", "camera": "Câmera", + "coordinates": "", "date_shot": "Data da foto", "exposure": "Exposição", "exposure_program": "Programa", @@ -278,6 +304,14 @@ } } }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "title": "" + }, "sharing": { "add_share": "Adicionar partilha", "copy_link": "Copiar link", @@ -295,6 +329,13 @@ "places": "Locais", "settings": "Configurações" }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, "title": { "loading_album": "Carregar Álbum", "login": "Login", diff --git a/ui/src/extractedTranslations/pt/translation_old.json b/ui/src/extractedTranslations/pt/translation_old.json new file mode 100644 index 0000000..8f73fa0 --- /dev/null +++ b/ui/src/extractedTranslations/pt/translation_old.json @@ -0,0 +1,19 @@ +{ + "people_page": { + "action_label": { + "change_label": null, + "merge_people": null, + "detach_images": null, + "move_faces": null + }, + "modal": { + "merge_people_groups": { + "description": "Todas as imagens neste grupo de caras serão juntas no grupo selecionado.", + "destination_table": { + "title": "Selecione a cara de destino" + }, + "title": "Juntar grupos de cara" + } + } + } +} diff --git a/ui/src/extractedTranslations/ru/translation.json b/ui/src/extractedTranslations/ru/translation.json index ecfa6c9..4f2bfc0 100644 --- a/ui/src/extractedTranslations/ru/translation.json +++ b/ui/src/extractedTranslations/ru/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "", - "detach_face": "", - "merge_face": "", + "detach_images": "", + "merge_people": "", "move_faces": "" }, "face_group": { @@ -268,7 +268,7 @@ "title": "" }, "media": { - "album": "", + "album_path": "", "exif": { "exposure_program": { "action_program": "Действие программа", @@ -310,6 +310,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/ru/translation_old.json b/ui/src/extractedTranslations/ru/translation_old.json index 9ade823..ae7cdd0 100644 --- a/ui/src/extractedTranslations/ru/translation_old.json +++ b/ui/src/extractedTranslations/ru/translation_old.json @@ -49,6 +49,15 @@ }, "tableselect_image_faces": { "search_images_placeholder": null + }, + "modal": { + "merge_people_groups": { + "description": "Все фотографии в этой группе лиц будут обьеденены с выбранной группой.", + "destination_table": { + "title": "Выберете конечное лицо" + }, + "title": "Обьединить группы лиц" + } } }, "settings": { @@ -99,6 +108,9 @@ "table_header": "Общий доступ", "delete": null, "more": null + }, + "media": { + "album": "" } }, "album_filter": { diff --git a/ui/src/extractedTranslations/sv/translation.json b/ui/src/extractedTranslations/sv/translation.json index bf4eed7..2917fe9 100644 --- a/ui/src/extractedTranslations/sv/translation.json +++ b/ui/src/extractedTranslations/sv/translation.json @@ -63,8 +63,8 @@ "people_page": { "action_label": { "change_label": "", - "detach_face": "", - "merge_face": "", + "detach_images": "", + "merge_people": "", "move_faces": "" }, "face_group": { @@ -263,7 +263,7 @@ "title": "" }, "media": { - "album": "", + "album_path": "", "exif": { "exposure_program": { "action_program": "Actionprogram", @@ -305,6 +305,11 @@ } }, "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/sv/translation_old.json b/ui/src/extractedTranslations/sv/translation_old.json index fb0943d..061609f 100644 --- a/ui/src/extractedTranslations/sv/translation_old.json +++ b/ui/src/extractedTranslations/sv/translation_old.json @@ -61,6 +61,13 @@ "title": null }, "title": null + }, + "merge_people_groups": { + "description": "", + "destination_table": { + "title": "" + }, + "title": "" } }, "table": { @@ -123,6 +130,9 @@ "mega_byte_plural": null, "tera_byte_plural": null } + }, + "media": { + "album": "" } }, "title": { diff --git a/ui/src/extractedTranslations/zh-CN/translation.json b/ui/src/extractedTranslations/zh-CN/translation.json index c60569f..c4f9997 100644 --- a/ui/src/extractedTranslations/zh-CN/translation.json +++ b/ui/src/extractedTranslations/zh-CN/translation.json @@ -61,17 +61,17 @@ "description": "简单及易用的照片库给个人服务器" }, "people_page": { + "action_label": { + "change_label": "", + "detach_images": "", + "merge_people": "", + "move_faces": "" + }, "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,30 +215,36 @@ }, "sidebar": { "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", "title_placeholder": "相册名称" }, "download": { "filesize": { - "byte": null, - "giga_byte": null, - "kilo_byte": null, - "mega_byte": null, - "tera_byte": null, - "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 + "byte": "", + "giga_byte": "", + "kilo_byte": "", + "mega_byte": "", + "tera_byte": "" }, "table_columns": { "dimensions": "尺寸", @@ -248,7 +254,11 @@ }, "title": "下载" }, + "location": { + "title": "" + }, "media": { + "album_path": "", "exif": { "exposure_program": { "action_program": "动态模式", @@ -264,19 +274,20 @@ }, "flash": { "auto": "自动", - "did_not_fire": null, - "fired": null, - "no_flash": null, - "no_flash_function": null, + "did_not_fire": "", + "fired": "", + "no_flash": "", + "no_flash_function": "", "off": "关闭", "on": "使用", "red_eye_reduction": "减轻红眼", - "return_detected": null, - "return_not_detected": null + "return_detected": "", + "return_not_detected": "" }, "name": { "aperture": "光圈", "camera": "相机", + "coordinates": "", "date_shot": "拍摄日期", "exposure": "曝光", "exposure_program": "模式", @@ -288,6 +299,14 @@ } } }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "title": "" + }, "sharing": { "add_share": "新增分享", "copy_link": "复制链接", @@ -305,6 +324,13 @@ "places": "地点", "settings": "设置" }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, "title": { "loading_album": "载入相册", "login": "登入", diff --git a/ui/src/extractedTranslations/zh-CN/translation_old.json b/ui/src/extractedTranslations/zh-CN/translation_old.json new file mode 100644 index 0000000..1f14223 --- /dev/null +++ b/ui/src/extractedTranslations/zh-CN/translation_old.json @@ -0,0 +1,42 @@ +{ + "people_page": { + "action_label": { + "change_label": null, + "merge_people": null, + "detach_images": null, + "move_faces": null + }, + "modal": { + "merge_people_groups": { + "description": "所有在此的脸孔群组将会合并到已选择的脸孔群组", + "destination_table": { + "title": "选择目标脸孔群组" + }, + "title": "合并脸孔群组" + } + } + }, + "sidebar": { + "download": { + "filesize": { + "byte": null, + "giga_byte": null, + "kilo_byte": null, + "mega_byte": null, + "tera_byte": null + } + }, + "media": { + "exif": { + "flash": { + "did_not_fire": null, + "fired": null, + "no_flash": null, + "no_flash_function": null, + "return_detected": null, + "return_not_detected": null + } + } + } + } +} diff --git a/ui/src/extractedTranslations/zh-HK/translation.json b/ui/src/extractedTranslations/zh-HK/translation.json index aa8fc83..4062476 100644 --- a/ui/src/extractedTranslations/zh-HK/translation.json +++ b/ui/src/extractedTranslations/zh-HK/translation.json @@ -61,17 +61,17 @@ "description": "簡單及易用的照片庫給個人伺服器" }, "people_page": { + "action_label": { + "change_label": "", + "detach_images": "", + "merge_people": "", + "move_faces": "" + }, "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,30 +215,36 @@ }, "sidebar": { "album": { + "album_cover": "", + "download": { + "high-resolutions": { + "description": "", + "title": "" + }, + "originals": { + "description": "", + "title": "" + }, + "thumbnails": { + "description": "", + "title": "" + }, + "web-videos": { + "description": "", + "title": "" + } + }, + "reset_cover": "", + "set_cover": "", "title_placeholder": "相簿名稱" }, "download": { "filesize": { - "byte": null, - "giga_byte": null, - "kilo_byte": null, - "mega_byte": null, - "tera_byte": null, - "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 + "byte": "", + "giga_byte": "", + "kilo_byte": "", + "mega_byte": "", + "tera_byte": "" }, "table_columns": { "dimensions": "尺寸", @@ -248,7 +254,11 @@ }, "title": "下載" }, + "location": { + "title": "" + }, "media": { + "album_path": "", "exif": { "exposure_program": { "action_program": "動態模式", @@ -264,19 +274,20 @@ }, "flash": { "auto": "自動", - "did_not_fire": null, - "fired": null, - "no_flash": null, - "no_flash_function": null, + "did_not_fire": "", + "fired": "", + "no_flash": "", + "no_flash_function": "", "off": "關閉", "on": "使用", "red_eye_reduction": "減輕紅眼", - "return_detected": null, - "return_not_detected": null + "return_detected": "", + "return_not_detected": "" }, "name": { "aperture": "光圈", "camera": "相機", + "coordinates": "", "date_shot": "拍攝日期", "exposure": "曝光", "exposure_program": "模式", @@ -288,6 +299,14 @@ } } }, + "people": { + "action_label": { + "detach_image": "", + "merge_face": "", + "move_face": "" + }, + "title": "" + }, "sharing": { "add_share": "新增分享", "copy_link": "複製連結", @@ -305,6 +324,13 @@ "places": "地點", "settings": "設定" }, + "timeline_filter": { + "date": { + "dropdown_all": "", + "dropdown_year": "", + "label": "" + } + }, "title": { "loading_album": "載入相簿", "login": "登入", diff --git a/ui/src/extractedTranslations/zh-HK/translation_old.json b/ui/src/extractedTranslations/zh-HK/translation_old.json new file mode 100644 index 0000000..c45a0a1 --- /dev/null +++ b/ui/src/extractedTranslations/zh-HK/translation_old.json @@ -0,0 +1,42 @@ +{ + "people_page": { + "action_label": { + "change_label": null, + "merge_people": null, + "detach_images": null, + "move_faces": null + }, + "modal": { + "merge_people_groups": { + "description": "所有在此的臉孔群組將會合併到已選擇的臉孔群組", + "destination_table": { + "title": "選擇目標臉孔群組" + }, + "title": "合併臉孔群組" + } + } + }, + "sidebar": { + "download": { + "filesize": { + "byte": null, + "giga_byte": null, + "kilo_byte": null, + "mega_byte": null, + "tera_byte": null + } + }, + "media": { + "exif": { + "flash": { + "did_not_fire": null, + "fired": null, + "no_flash": null, + "no_flash_function": null, + "return_detected": null, + "return_not_detected": null + } + } + } + } +} From df57f55ac41b0cba32730e426dc0175639f0b850 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 21 Oct 2021 15:57:41 +0200 Subject: [PATCH 08/11] Add functionality to sidebar people menu --- ui/src/Pages/PeoplePage/PeoplePage.test.tsx | 18 +- ui/src/Pages/PeoplePage/PeoplePage.tsx | 49 +++-- .../SingleFaceGroup/DetachImageFacesModal.tsx | 70 +++++-- .../SingleFaceGroup/FaceGroupTitle.tsx | 7 +- .../SingleFaceGroup/MergeFaceGroupsModal.tsx | 20 +- .../SingleFaceGroup/MoveImageFacesModal.tsx | 22 ++- .../sidebar/MediaSidebar/MediaSidebar.tsx | 3 +- .../MediaSidebar/MediaSidebarPeople.tsx | 176 +++++++++++++----- .../__generated__/sidebarMediaQuery.ts | 1 + .../extractedTranslations/da/translation.json | 9 +- .../extractedTranslations/de/translation.json | 1 + .../extractedTranslations/en/translation.json | 1 + .../extractedTranslations/es/translation.json | 1 + .../extractedTranslations/fr/translation.json | 1 + .../extractedTranslations/it/translation.json | 1 + .../extractedTranslations/pl/translation.json | 1 + .../extractedTranslations/pt/translation.json | 1 + .../extractedTranslations/ru/translation.json | 1 + .../extractedTranslations/sv/translation.json | 1 + .../zh-CN/translation.json | 1 + .../zh-HK/translation.json | 1 + ui/src/helpers/utils.ts | 5 +- 22 files changed, 286 insertions(+), 105 deletions(-) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx index 3a6e9b3..dfe83b0 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx @@ -144,7 +144,11 @@ describe('FaceDetails component', () => { render( - + ) @@ -159,7 +163,11 @@ describe('FaceDetails component', () => { render( - + ) @@ -190,7 +198,11 @@ describe('FaceDetails component', () => { ] render( - + ) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.tsx b/ui/src/Pages/PeoplePage/PeoplePage.tsx index e60135d..7d2f1b2 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx @@ -19,6 +19,7 @@ import { myFaces_myFaceGroups, } from './__generated__/myFaces' import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces' +import { tailwindClassNames } from '../../helpers/utils' export const MY_FACES_QUERY = gql` query myFaces($limit: Int, $offset: Int) { @@ -65,15 +66,8 @@ const RECOGNIZE_UNLABELED_FACES_MUTATION = gql` } ` -const FaceDetailsWrapper = styled.div<{ labeled: boolean }>` +const FaceDetailsWrapper = styled.span<{ labeled: boolean }>` color: ${({ labeled }) => (labeled ? 'black' : '#aaa')}; - width: 150px; - margin: 12px auto 24px; - text-align: center; - display: block; - background: none; - border: none; - cursor: pointer; &:hover, &:focus-visible { @@ -82,12 +76,26 @@ const FaceDetailsWrapper = styled.div<{ labeled: boolean }>` ` type FaceDetailsProps = { - group: myFaces_myFaceGroups + group: { + __typename: 'FaceGroup' + id: string + label: string | null + imageFaceCount: number + } + className?: string + textFieldClassName?: string + editLabel: boolean + setEditLabel: React.Dispatch> } -export const FaceDetails = ({ group }: FaceDetailsProps) => { +export const FaceDetails = ({ + group, + className, + textFieldClassName, + editLabel, + setEditLabel, +}: FaceDetailsProps) => { const { t } = useTranslation() - const [editLabel, setEditLabel] = useState(false) const [inputValue, setInputValue] = useState(group.label ?? '') const inputRef = createRef() @@ -126,11 +134,15 @@ export const FaceDetails = ({ group }: FaceDetailsProps) => { if (!editLabel) { label = ( setEditLabel(true)} > {group.imageFaceCount} - {/* */} @@ -138,9 +150,9 @@ export const FaceDetails = ({ group }: FaceDetailsProps) => { ) } else { label = ( - + { const previewFace = group.imageFaces[0] + const [editLabel, setEditLabel] = useState(false) return (
    - +
    ) } diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx index 2cd2b9b..221e8da 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx @@ -1,4 +1,4 @@ -import { gql, useMutation } from '@apollo/client' +import { BaseMutationOptions, gql, useMutation } from '@apollo/client' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' @@ -28,16 +28,50 @@ const DETACH_IMAGE_FACES_MUTATION = gql` } ` +export const useDetachImageFaces = ( + mutationOptions: BaseMutationOptions< + detachImageFaces, + detachImageFacesVariables + > +) => { + const [detachImageFacesMutation] = useMutation< + detachImageFaces, + detachImageFacesVariables + >(DETACH_IMAGE_FACES_MUTATION, mutationOptions) + + return async ( + selectedImageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + ) => { + const faceIDs = selectedImageFaces.map(face => face.id) + + const result = await detachImageFacesMutation({ + variables: { + faceIDs, + }, + }) + + return result + } +} + type DetachImageFacesModalProps = { open: boolean setOpen(open: boolean): void faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup + selectedImageFaces?: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] } const DetachImageFacesModal = ({ open, setOpen, faceGroup, + selectedImageFaces: selectedImageFacesProp, }: DetachImageFacesModalProps) => { const { t } = useTranslation() @@ -46,10 +80,7 @@ const DetachImageFacesModal = ({ >([]) const history = useHistory() - const [detachImageFacesMutation] = useMutation< - detachImageFaces, - detachImageFacesVariables - >(DETACH_IMAGE_FACES_MUTATION, { + const detachImageFacesMutation = useDetachImageFaces({ refetchQueries: [ { query: MY_FACES_QUERY, @@ -57,6 +88,19 @@ const DetachImageFacesModal = ({ ], }) + const detachImageFaces = () => { + detachImageFacesMutation(selectedImageFaces).then(({ data }) => { + if (isNil(data)) throw new Error('Expected data not to be null') + setOpen(false) + history.push(`/people/${data.detachImageFaces.id}`) + }) + } + + useEffect(() => { + if (isNil(selectedImageFacesProp)) return + setSelectedImageFaces(selectedImageFacesProp) + }, [selectedImageFacesProp]) + useEffect(() => { if (!open) { setSelectedImageFaces([]) @@ -65,20 +109,6 @@ const DetachImageFacesModal = ({ if (open == false) return null - const detachImageFaces = () => { - const faceIDs = selectedImageFaces.map(face => face.id) - - detachImageFacesMutation({ - variables: { - faceIDs, - }, - }).then(({ data }) => { - if (isNil(data)) throw new Error('Expected data not to be null') - setOpen(false) - history.push(`/people/${data.detachImageFaces.id}`) - }) - } - const imageFaces = faceGroup?.imageFaces ?? [] return ( @@ -94,7 +124,7 @@ const DetachImageFacesModal = ({ actions={[ { key: 'cancel', - label: 'Cancel', + label: t('general.action.cancel', 'Cancel'), onClick: () => setOpen(false), }, { diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx index b49ed8f..0e193be 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/FaceGroupTitle.tsx @@ -8,7 +8,7 @@ import React, { import { useTranslation } from 'react-i18next' import { isNil } from '../../../helpers/utils' import { Button, TextField } from '../../../primitives/form/Input' -import { SET_GROUP_LABEL_MUTATION } from '../PeoplePage' +import { MY_FACES_QUERY, SET_GROUP_LABEL_MUTATION } from '../PeoplePage' import { setGroupLabel, setGroupLabelVariables, @@ -112,6 +112,11 @@ const FaceGroupTitle = ({ faceGroup }: FaceGroupTitleProps) => { open={mergeModalOpen} setOpen={setMergeModalOpen} sourceFaceGroup={faceGroup} + refetchQueries={[ + { + query: MY_FACES_QUERY, + }, + ]} /> { const { t } = useTranslation() - const [selectedFaceGroup, setSelectedFaceGroup] = - useState(null) + const [selectedFaceGroup, setSelectedFaceGroup] = useState< + myFaces_myFaceGroups | singleFaceGroup_faceGroup | null + >(null) const history = useHistory() const { data } = useQuery(MY_FACES_QUERY) @@ -50,11 +56,7 @@ const MergeFaceGroupsModal = ({ combineFaces, combineFacesVariables >(COMBINE_FACES_MUTATION, { - refetchQueries: [ - { - query: MY_FACES_QUERY, - }, - ], + refetchQueries: refetchQueries, }) if (open == false) return null diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx b/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx index 6fc6494..4eee200 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal.tsx @@ -40,20 +40,26 @@ type MoveImageFacesModalProps = { open: boolean setOpen: React.Dispatch> faceGroup: singleFaceGroup_faceGroup + preselectedImageFaces?: ( + | singleFaceGroup_faceGroup_imageFaces + | myFaces_myFaceGroups_imageFaces + )[] } const MoveImageFacesModal = ({ open, setOpen, faceGroup, + preselectedImageFaces, }: MoveImageFacesModalProps) => { const { t } = useTranslation() const [selectedImageFaces, setSelectedImageFaces] = useState< (singleFaceGroup_faceGroup_imageFaces | myFaces_myFaceGroups_imageFaces)[] >([]) - const [selectedFaceGroup, setSelectedFaceGroup] = - useState(null) + const [selectedFaceGroup, setSelectedFaceGroup] = useState< + myFaces_myFaceGroups | singleFaceGroup_faceGroup | null + >(null) const [imagesSelected, setImagesSelected] = useState(false) const history = useHistory() @@ -68,8 +74,16 @@ const MoveImageFacesModal = ({ ], }) - const [loadFaceGroups, { data: faceGroupsData }] = - useLazyQuery(MY_FACES_QUERY) + const [loadFaceGroups, { data: faceGroupsData }] = useLazyQuery< + myFaces, + myFacesVariables + >(MY_FACES_QUERY) + + useEffect(() => { + if (isNil(preselectedImageFaces)) return + setSelectedImageFaces(preselectedImageFaces) + setImagesSelected(true) + }, [preselectedImageFaces]) useEffect(() => { if (imagesSelected) { diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx index 1830177..fdadac8 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.tsx @@ -31,7 +31,7 @@ import { } from './__generated__/sidebarMediaQuery' import { BreadcrumbList } from '../../album/AlbumTitle' -const SIDEBAR_MEDIA_QUERY = gql` +export const SIDEBAR_MEDIA_QUERY = gql` query sidebarMediaQuery($id: ID!) { media(id: $id) { id @@ -99,6 +99,7 @@ const SIDEBAR_MEDIA_QUERY = gql` faceGroup { id label + imageFaceCount } media { id diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx index e4be0f7..3c44d42 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx @@ -1,34 +1,46 @@ -import React from 'react' +import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import { Link } from 'react-router-dom' +import { Link, useHistory } from 'react-router-dom' import FaceCircleImage from '../../../Pages/PeoplePage/FaceCircleImage' import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents' -import { MediaSidebarMedia } from './MediaSidebar' +import { MediaSidebarMedia, SIDEBAR_MEDIA_QUERY } from './MediaSidebar' import { sidebarMediaQuery_media_faces } from './__generated__/sidebarMediaQuery' import { ReactComponent as PeopleDotsIcon } from './icons/peopleDotsIcon.svg' import { Menu } from '@headlessui/react' import { Button } from '../../../primitives/form/Input' import { ArrowPopoverPanel } from '../Sharing' -import { tailwindClassNames } from '../../../helpers/utils' +import { isNil, tailwindClassNames } from '../../../helpers/utils' +import MergeFaceGroupsModal from '../../../Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal' +import { useDetachImageFaces } from '../../../Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal' +import MoveImageFacesModal from '../../../Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal' +import { FaceDetails } from '../../../Pages/PeoplePage/PeoplePage' type PersonMoreMenuItemProps = { label: string className?: string + onClick(): void } -const PersonMoreMenuItem = ({ label, className }: PersonMoreMenuItemProps) => { +const PersonMoreMenuItem = ({ + label, + className, + onClick, +}: PersonMoreMenuItemProps) => { return ( {({ active }) => ( - {label} - + )} ) @@ -36,40 +48,105 @@ const PersonMoreMenuItem = ({ label, className }: PersonMoreMenuItemProps) => { type PersonMoreMenuProps = { face: sidebarMediaQuery_media_faces + setChangeLabel: React.Dispatch> + className?: string } -const PersonMoreMenu = ({ face }: PersonMoreMenuProps) => { +const PersonMoreMenu = ({ + face, + setChangeLabel, + className, +}: PersonMoreMenuProps) => { const { t } = useTranslation() - face + const [mergeModalOpen, setMergeModalOpen] = useState(false) + const [moveModalOpen, setMoveModalOpen] = useState(false) + + const refetchQueries = [ + { + query: SIDEBAR_MEDIA_QUERY, + variables: { + id: face.media.id, + }, + }, + ] + + const history = useHistory() + const detachImageFaceMutation = useDetachImageFaces({ + refetchQueries, + }) + + const modals = ( + <> + + + + ) + + const detachImageFace = () => { + if ( + !confirm( + t( + 'sidebar.people.confirm_image_detach', + 'Are you sure you want to detach this image?' + ) + ) + ) + return + detachImageFaceMutation([face]).then(({ data }) => { + if (isNil(data)) throw new Error('Expected data not to be null') + history.push(`/people/${data.detachImageFaces.id}`) + }) + } + return ( - - - - - - - - - - - - - + <> + + + + + + + setChangeLabel(true)} + className="border-b" + label={t('people_page.action_label.change_label', 'Change label')} + /> + setMergeModalOpen(true)} + className="border-b" + label={t('sidebar.people.action_label.merge_face', 'Merge face')} + /> + detachImageFace()} + className="border-b" + label={t( + 'sidebar.people.action_label.detach_image', + 'Detach image' + )} + /> + setMoveModalOpen(true)} + label={t('sidebar.people.action_label.move_face', 'Move face')} + /> + + + + {modals} + ) } @@ -78,21 +155,28 @@ type MediaSidebarFaceProps = { } const MediaSidebarPerson = ({ face }: MediaSidebarFaceProps) => { - const { t } = useTranslation() + const [changeLabel, setChangeLabel] = useState(false) return (
  • -
    - {face.faceGroup.label ?? - t('people_page.face_group.unlabeled', 'Unlabeled')} - +
    + + {!changeLabel && ( + + )}
  • ) diff --git a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts index 01b6199..13d8552 100644 --- a/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts +++ b/ui/src/components/sidebar/MediaSidebar/__generated__/sidebarMediaQuery.ts @@ -153,6 +153,7 @@ export interface sidebarMediaQuery_media_faces_faceGroup { __typename: 'FaceGroup' id: string label: string | null + imageFaceCount: number } export interface sidebarMediaQuery_media_faces_media_thumbnail { diff --git a/ui/src/extractedTranslations/da/translation.json b/ui/src/extractedTranslations/da/translation.json index 525e3c0..61a3949 100644 --- a/ui/src/extractedTranslations/da/translation.json +++ b/ui/src/extractedTranslations/da/translation.json @@ -263,7 +263,7 @@ "title": "Lokation" }, "media": { - "album_path": "", + "album_path": "Album sti", "exif": { "exposure_program": { "action_program": "Actionprogram", @@ -306,10 +306,11 @@ }, "people": { "action_label": { - "detach_image": "", - "merge_face": "", - "move_face": "" + "detach_image": "Løsriv billede", + "merge_face": "Sammenflet person", + "move_face": "Flyt person" }, + "confirm_image_detach": "Er du sikker på at du vil løsrive dette billede?", "title": "Personer" }, "sharing": { diff --git a/ui/src/extractedTranslations/de/translation.json b/ui/src/extractedTranslations/de/translation.json index 1fc27d8..a5157e5 100644 --- a/ui/src/extractedTranslations/de/translation.json +++ b/ui/src/extractedTranslations/de/translation.json @@ -310,6 +310,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/en/translation.json b/ui/src/extractedTranslations/en/translation.json index fabeafe..791c7d2 100644 --- a/ui/src/extractedTranslations/en/translation.json +++ b/ui/src/extractedTranslations/en/translation.json @@ -310,6 +310,7 @@ "merge_face": "Merge face", "move_face": "Move face" }, + "confirm_image_detach": "Are you sure you want to detach this image?", "title": "People" }, "sharing": { diff --git a/ui/src/extractedTranslations/es/translation.json b/ui/src/extractedTranslations/es/translation.json index 983e1c7..8003028 100644 --- a/ui/src/extractedTranslations/es/translation.json +++ b/ui/src/extractedTranslations/es/translation.json @@ -310,6 +310,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/fr/translation.json b/ui/src/extractedTranslations/fr/translation.json index d3fe60d..51ca9cd 100644 --- a/ui/src/extractedTranslations/fr/translation.json +++ b/ui/src/extractedTranslations/fr/translation.json @@ -310,6 +310,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/it/translation.json b/ui/src/extractedTranslations/it/translation.json index 1f942bf..528d835 100644 --- a/ui/src/extractedTranslations/it/translation.json +++ b/ui/src/extractedTranslations/it/translation.json @@ -310,6 +310,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/pl/translation.json b/ui/src/extractedTranslations/pl/translation.json index 5fcffc3..8e7186f 100644 --- a/ui/src/extractedTranslations/pl/translation.json +++ b/ui/src/extractedTranslations/pl/translation.json @@ -315,6 +315,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/pt/translation.json b/ui/src/extractedTranslations/pt/translation.json index c9c5e16..7e90e82 100644 --- a/ui/src/extractedTranslations/pt/translation.json +++ b/ui/src/extractedTranslations/pt/translation.json @@ -310,6 +310,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/ru/translation.json b/ui/src/extractedTranslations/ru/translation.json index 4f2bfc0..2665ec8 100644 --- a/ui/src/extractedTranslations/ru/translation.json +++ b/ui/src/extractedTranslations/ru/translation.json @@ -315,6 +315,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/sv/translation.json b/ui/src/extractedTranslations/sv/translation.json index 2917fe9..2fcfeaa 100644 --- a/ui/src/extractedTranslations/sv/translation.json +++ b/ui/src/extractedTranslations/sv/translation.json @@ -310,6 +310,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/zh-CN/translation.json b/ui/src/extractedTranslations/zh-CN/translation.json index c4f9997..d17e811 100644 --- a/ui/src/extractedTranslations/zh-CN/translation.json +++ b/ui/src/extractedTranslations/zh-CN/translation.json @@ -305,6 +305,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/extractedTranslations/zh-HK/translation.json b/ui/src/extractedTranslations/zh-HK/translation.json index 4062476..479cc16 100644 --- a/ui/src/extractedTranslations/zh-HK/translation.json +++ b/ui/src/extractedTranslations/zh-HK/translation.json @@ -305,6 +305,7 @@ "merge_face": "", "move_face": "" }, + "confirm_image_detach": "", "title": "" }, "sharing": { diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index 6f9042e..8bf66d7 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -1,5 +1,5 @@ import classNames, { Argument as ClassNamesArg } from 'classnames' -import { overrideTailwindClasses } from 'tailwind-override' +// import { overrideTailwindClasses } from 'tailwind-override' /* eslint-disable @typescript-eslint/no-explicit-any */ export interface DebouncedFn any> { @@ -45,5 +45,6 @@ export function exhaustiveCheck(value: never) { } export function tailwindClassNames(...args: ClassNamesArg[]) { - return overrideTailwindClasses(classNames(args)) + // return overrideTailwindClasses(classNames(args)) + return classNames(args) } From b20229f9950b49cb1802b1a65e4640b327b2322e Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 21 Oct 2021 16:46:08 +0200 Subject: [PATCH 09/11] Flip people menu for first person --- .../MediaSidebar/MediaSidebarPeople.tsx | 12 ++++++---- ui/src/components/sidebar/Sharing.tsx | 24 ++++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx index 3c44d42..8b87488 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarPeople.tsx @@ -50,9 +50,11 @@ type PersonMoreMenuProps = { face: sidebarMediaQuery_media_faces setChangeLabel: React.Dispatch> className?: string + menuFlipped: boolean } const PersonMoreMenu = ({ + menuFlipped, face, setChangeLabel, className, @@ -119,7 +121,7 @@ const PersonMoreMenu = ({ - + setChangeLabel(true)} className="border-b" @@ -152,9 +154,10 @@ const PersonMoreMenu = ({ type MediaSidebarFaceProps = { face: sidebarMediaQuery_media_faces + menuFlipped: boolean } -const MediaSidebarPerson = ({ face }: MediaSidebarFaceProps) => { +const MediaSidebarPerson = ({ face, menuFlipped }: MediaSidebarFaceProps) => { const [changeLabel, setChangeLabel] = useState(false) return ( @@ -172,6 +175,7 @@ const MediaSidebarPerson = ({ face }: MediaSidebarFaceProps) => { /> {!changeLabel && ( { const { t } = useTranslation() - const faceElms = (media.faces ?? []).map(face => ( - + const faceElms = (media.faces ?? []).map((face, i) => ( + )) if (faceElms.length == 0) return null diff --git a/ui/src/components/sidebar/Sharing.tsx b/ui/src/components/sidebar/Sharing.tsx index e0100f3..04d3a29 100644 --- a/ui/src/components/sidebar/Sharing.tsx +++ b/ui/src/components/sidebar/Sharing.tsx @@ -108,18 +108,36 @@ const DELETE_SHARE_MUTATION = gql` export const ArrowPopoverPanel = styled.div.attrs({ className: - 'absolute right-6 -top-3 bg-white rounded shadow-md border border-gray-200', -})<{ width: number }>` + 'absolute -top-3 bg-white rounded shadow-md border border-gray-200 z-10', +})<{ width: number; flipped?: boolean }>` width: ${({ width }) => width}px; + ${({ flipped }) => + flipped + ? ` + left: 32px; + ` + : ` + right: 24px; + `} + &::after { content: ''; position: absolute; top: 18px; - right: -7px; width: 8px; height: 14px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 14'%3E%3Cpolyline stroke-width='1' stroke='%23E2E2E2' fill='%23FFFFFF' points='1 0 7 7 1 14'%3E%3C/polyline%3E%3C/svg%3E"); + + ${({ flipped }) => + flipped + ? ` + left: -7px; + transform: rotate(180deg); + ` + : ` + right: -7px; + `} } ` From fb261cc299c19bb5b69294553d8125c7f88155ce Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 21 Oct 2021 17:12:55 +0200 Subject: [PATCH 10/11] Fix ui tests --- ui/src/Pages/PeoplePage/PeoplePage.test.tsx | 35 ++++++++++++++++--- ui/src/Pages/PeoplePage/PeoplePage.tsx | 2 +- ...debar.test.js => MediaSidebarExif.test.js} | 8 ++--- 3 files changed, 35 insertions(+), 10 deletions(-) rename ui/src/components/sidebar/MediaSidebar/{MediaSidebar.test.js => MediaSidebarExif.test.js} (94%) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx index dfe83b0..efef1f8 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx @@ -2,6 +2,7 @@ import React from 'react' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import PeoplePage, { FaceDetails, + FaceGroup, MY_FACES_QUERY, SET_GROUP_LABEL_MUTATION, } from './PeoplePage' @@ -198,11 +199,9 @@ describe('FaceDetails component', () => { ] render( - + + + ) @@ -223,4 +222,30 @@ describe('FaceDetails component', () => { expect(graphqlMocks[0].newData).toHaveBeenCalled() }) }) + + test('cancel add label to face group', async () => { + render( + + + + + + ) + + const btn = screen.getByRole('button') + expect(btn).toBeInTheDocument() + expect(screen.queryByRole('textbox')).not.toBeInTheDocument() + expect(screen.queryByText('Unlabeled')).toBeInTheDocument() + + fireEvent.click(btn) + + const input = screen.getByRole('textbox') + expect(input).toBeInTheDocument() + expect(input).toHaveValue('') + + fireEvent.change(input, { target: { value: 'John Doe' } }) + fireEvent.keyDown(input, { key: 'Escape', code: 'Escape' }) + + expect(screen.queryByText('Unlabeled')).toBeInTheDocument() + }) }) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.tsx b/ui/src/Pages/PeoplePage/PeoplePage.tsx index 7d2f1b2..79bab3c 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.tsx +++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx @@ -193,7 +193,7 @@ type FaceGroupProps = { group: myFaces_myFaceGroups } -const FaceGroup = ({ group }: FaceGroupProps) => { +export const FaceGroup = ({ group }: FaceGroupProps) => { const previewFace = group.imageFaces[0] const [editLabel, setEditLabel] = useState(false) diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.js b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.js similarity index 94% rename from ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.js rename to ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.js index 7ec5f15..5af8df9 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.js +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.js @@ -1,8 +1,8 @@ import React from 'react' import { render, screen } from '@testing-library/react' -import { MetadataInfo } from './MediaSidebar' +import ExifDetails from './MediaSidebarExif' -describe('MetadataInfo', () => { +describe('ExifDetails', () => { test('without EXIF information', async () => { const media = { id: '1730', @@ -25,7 +25,7 @@ describe('MetadataInfo', () => { __typename: 'Media', } - render() + render() expect(screen.queryByText('Camera')).not.toBeInTheDocument() expect(screen.queryByText('Maker')).not.toBeInTheDocument() @@ -61,7 +61,7 @@ describe('MetadataInfo', () => { __typename: 'Media', } - render() + render() expect(screen.getByText('Camera')).toBeInTheDocument() expect(screen.getByText('Canon EOS R')).toBeInTheDocument() From 11c26cd68ef40b496242028532a43643de1691c0 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 21 Oct 2021 18:05:11 +0200 Subject: [PATCH 11/11] Add test for MediaSidebar --- ui/src/Pages/SharePage/AlbumSharePage.tsx | 5 ++ ...Route.test.js => AuthorizedRoute.test.tsx} | 8 +- ui/src/components/sidebar/AlbumCovers.tsx | 6 ++ .../MediaSidebar/MediaSidebar.test.tsx | 87 +++++++++++++++++++ ...Exif.test.js => MediaSidebarExif.test.tsx} | 20 ++++- 5 files changed, 120 insertions(+), 6 deletions(-) rename ui/src/components/routes/{AuthorizedRoute.test.js => AuthorizedRoute.test.tsx} (84%) create mode 100644 ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.tsx rename ui/src/components/sidebar/MediaSidebar/{MediaSidebarExif.test.js => MediaSidebarExif.test.tsx} (83%) diff --git a/ui/src/Pages/SharePage/AlbumSharePage.tsx b/ui/src/Pages/SharePage/AlbumSharePage.tsx index 1549376..6635d12 100644 --- a/ui/src/Pages/SharePage/AlbumSharePage.tsx +++ b/ui/src/Pages/SharePage/AlbumSharePage.tsx @@ -66,6 +66,7 @@ export const SHARE_ALBUM_QUERY = gql` url } exif { + id camera maker lens @@ -76,6 +77,10 @@ export const SHARE_ALBUM_QUERY = gql` focalLength flash exposureProgram + coordinates { + latitude + longitude + } } } } diff --git a/ui/src/components/routes/AuthorizedRoute.test.js b/ui/src/components/routes/AuthorizedRoute.test.tsx similarity index 84% rename from ui/src/components/routes/AuthorizedRoute.test.js rename to ui/src/components/routes/AuthorizedRoute.test.tsx index 7bd8371..7e87756 100644 --- a/ui/src/components/routes/AuthorizedRoute.test.js +++ b/ui/src/components/routes/AuthorizedRoute.test.tsx @@ -7,11 +7,15 @@ import * as authentication from '../../helpers/authentication' jest.mock('../../helpers/authentication.ts') +const authToken = authentication.authToken as jest.Mock< + ReturnType +> + describe('AuthorizedRoute component', () => { const AuthorizedComponent = () =>
    authorized content
    test('not logged in', async () => { - authentication.authToken.mockImplementation(() => null) + authToken.mockImplementation(() => null) render( @@ -24,7 +28,7 @@ describe('AuthorizedRoute component', () => { }) test('logged in', async () => { - authentication.authToken.mockImplementation(() => 'token-here') + authToken.mockImplementation(() => 'token-here') render( diff --git a/ui/src/components/sidebar/AlbumCovers.tsx b/ui/src/components/sidebar/AlbumCovers.tsx index d00cca7..4b0d7ab 100644 --- a/ui/src/components/sidebar/AlbumCovers.tsx +++ b/ui/src/components/sidebar/AlbumCovers.tsx @@ -12,6 +12,7 @@ import { resetAlbumCover, resetAlbumCoverVariables, } from './__generated__/resetAlbumCover' +import { authToken } from '../../helpers/authentication' const RESET_ALBUM_COVER_MUTATION = gql` mutation resetAlbumCover($albumID: ID!) { @@ -62,6 +63,11 @@ export const SidebarPhotoCover = ({ cover_id }: SidebarPhotoCoverProps) => { setButtonDisabled(false) }, [cover_id]) + // hide when not authenticated + if (!authToken()) { + return null + } + return ( diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.tsx b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.tsx new file mode 100644 index 0000000..6c0cf62 --- /dev/null +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebar.test.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { MockedProvider } from '@apollo/client/testing' +import MediaSidebar, { MediaSidebarMedia } from './MediaSidebar' +import { MediaType } from '../../../__generated__/globalTypes' +import { MemoryRouter } from 'react-router' + +import * as authentication from '../../../helpers/authentication' + +jest.mock('../../../helpers/authentication.ts') + +const authToken = authentication.authToken as jest.Mock< + ReturnType +> + +describe('MediaSidebar', () => { + const media: MediaSidebarMedia = { + __typename: 'Media', + id: '6867', + title: '122A6069.jpg', + type: MediaType.Photo, + thumbnail: { + __typename: 'MediaURL', + url: 'http://localhost:4001/photo/thumbnail.jpg', + width: 1024, + height: 839, + }, + highRes: { + __typename: 'MediaURL', + url: 'http://localhost:4001/photo/highres.jpg', + width: 5322, + height: 4362, + }, + videoWeb: null, + album: { + __typename: 'Album', + id: '2294', + title: 'album_name', + }, + } + + test('render sample image, unauthorized', () => { + authToken.mockImplementation(() => null) + + render( + + + + + + ) + + expect(screen.getByText('122A6069.jpg')).toBeInTheDocument() + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + 'http://localhost:4001/photo/highres.jpg' + ) + + expect( + screen.queryByText('Set as album cover photo') + ).not.toBeInTheDocument() + expect(screen.queryByText('Sharing options')).not.toBeInTheDocument() + }) + + test('render sample image, authorized', () => { + authToken.mockImplementation(() => 'token-here') + + render( + + + + + + ) + + expect(screen.getByText('122A6069.jpg')).toBeInTheDocument() + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + 'http://localhost:4001/photo/highres.jpg' + ) + + expect(screen.getByText('Set as album cover photo')).toBeInTheDocument() + expect(screen.getByText('Album path')).toBeInTheDocument() + + screen.debug() + }) +}) diff --git a/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.js b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.tsx similarity index 83% rename from ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.js rename to ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.tsx index 5af8df9..0e3b002 100644 --- a/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.js +++ b/ui/src/components/sidebar/MediaSidebar/MediaSidebarExif.test.tsx @@ -1,13 +1,15 @@ import React from 'react' import { render, screen } from '@testing-library/react' import ExifDetails from './MediaSidebarExif' +import { MediaSidebarMedia } from './MediaSidebar' +import { MediaType } from '../../../__generated__/globalTypes' describe('ExifDetails', () => { test('without EXIF information', async () => { - const media = { + const media: MediaSidebarMedia = { id: '1730', title: 'media_name.jpg', - type: 'Photo', + type: MediaType.Photo, exif: { id: '0', camera: null, @@ -20,6 +22,7 @@ describe('ExifDetails', () => { focalLength: null, flash: null, exposureProgram: null, + coordinates: null, __typename: 'MediaEXIF', }, __typename: 'Media', @@ -37,13 +40,14 @@ describe('ExifDetails', () => { expect(screen.queryByText('ISO')).not.toBeInTheDocument() expect(screen.queryByText('Focal length')).not.toBeInTheDocument() expect(screen.queryByText('Flash')).not.toBeInTheDocument() + expect(screen.queryByText('Coordinates')).not.toBeInTheDocument() }) test('with EXIF information', async () => { - const media = { + const media: MediaSidebarMedia = { id: '1730', title: 'media_name.jpg', - type: 'Photo', + type: MediaType.Photo, exif: { id: '1666', camera: 'Canon EOS R', @@ -56,6 +60,11 @@ describe('ExifDetails', () => { focalLength: 24, flash: 9, exposureProgram: 3, + coordinates: { + __typename: 'Coordinates', + latitude: 41.40338, + longitude: 2.17403, + }, __typename: 'MediaEXIF', }, __typename: 'Media', @@ -94,5 +103,8 @@ describe('ExifDetails', () => { expect(screen.getByText('Flash')).toBeInTheDocument() expect(screen.getByText('On, Fired')).toBeInTheDocument() + + expect(screen.getByText('Coordinates')).toBeInTheDocument() + expect(screen.getByText('41.40338, 2.17403')).toBeInTheDocument() }) })