diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index b30c1010..bc6f3103 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": "Tilføj navn", + "change_label": "Ændre navn", + "detach_face": "Løsriv ansigter", + "merge_face": "Sammenflet ansigter", + "move_faces": "Flyt ansigter" + }, "label_placeholder": "Navn", - "unlabeled": "Ikke navngivet" + "unlabeled": "Ikke navngivet", + "unlabeled_person": "Ikke navngivet person" }, - "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter" + "modal": { + "action": { + "merge": "Sammenflet" + }, + "detach_image_faces": { + "action": { + "detach": "Løsriv billeder", + "select_images": "Vælg billeder som skal løsrives" + }, + "description": "Løsriv valgte billeder fra denne gruppe og flyt dem til sin egen gruppe", + "title": "Løsriv Billeder" + }, + "merge_face_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" + }, + "move_image_faces": { + "description": "Flyt valgte billeder fra denne gruppe til en anden gruppe", + "destination_face_group_table": { + "move_action": "Flyt ansigter", + "title": "Vælg destinationsgruppe" + }, + "image_select_table": { + "next_action": "Næste", + "title": "Vælg billeder som skal flyttes" + }, + "title": "Flyt Ansigter" + } + }, + "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter", + "table": { + "select_face_group": { + "search_faces_placeholder": "Søg ansigter..." + }, + "select_image_faces": { + "search_images_placeholder": "Søg billeder..." + } + } }, "photos_page": { "title": "Billeder" @@ -156,9 +204,9 @@ "title": "Brugere" }, "version_info": { - "build_date_title": null, - "title": null, - "version_title": null + "build_date_title": "Dato for udgivelse", + "title": "Photoview Version", + "version_title": "Udgivelsesversion" } }, "share_page": { @@ -252,7 +300,7 @@ }, "title": { "loading_album": "Loader album", - "login": null, + "login": "Logind", "people": "Personer", "settings": "Indstillinger" } diff --git a/ui/extractedTranslations/de/translation.json b/ui/extractedTranslations/de/translation.json index 558a37b6..542e988e 100644 --- a/ui/extractedTranslations/de/translation.json +++ b/ui/extractedTranslations/de/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Zuordnung", - "unlabeled": "Nicht zugeordnet" + "unlabeled": "Nicht zugeordnet", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Fotos" diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index 3d9820c0..21881e01 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": "Add Label", + "change_label": "Change Label", + "detach_face": "Detach Face", + "merge_face": "Merge Face", + "move_faces": "Move Faces" + }, "label_placeholder": "Label", - "unlabeled": "Unlabeled" + "unlabeled": "Unlabeled", + "unlabeled_person": "Unlabeled person" }, - "recognize_unlabeled_faces_button": "Recognize unlabeled faces" + "modal": { + "action": { + "merge": "Merge" + }, + "detach_image_faces": { + "action": { + "detach": "Detach image faces", + "select_images": "Select images to detach" + }, + "description": "Detach selected images of this face group and move them to a new face groups", + "title": "Detach Image Faces" + }, + "merge_face_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" + }, + "move_image_faces": { + "description": "Move selected images of this face group to another face group", + "destination_face_group_table": { + "move_action": "Move image faces", + "title": "Select destination face group" + }, + "image_select_table": { + "next_action": "Next", + "title": "Select images to move" + }, + "title": "Move Image Faces" + } + }, + "recognize_unlabeled_faces_button": "Recognize unlabeled faces", + "table": { + "select_face_group": { + "search_faces_placeholder": "Search faces..." + }, + "select_image_faces": { + "search_images_placeholder": "Search images..." + } + } }, "photos_page": { "title": "Photos" diff --git a/ui/extractedTranslations/es/translation.json b/ui/extractedTranslations/es/translation.json index b52e82a9..3be1421a 100644 --- a/ui/extractedTranslations/es/translation.json +++ b/ui/extractedTranslations/es/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Etiqueta", - "unlabeled": "Sin etiquetar" + "unlabeled": "Sin etiquetar", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Fotos" diff --git a/ui/extractedTranslations/fr/translation.json b/ui/extractedTranslations/fr/translation.json index 2078abeb..8a847c6b 100644 --- a/ui/extractedTranslations/fr/translation.json +++ b/ui/extractedTranslations/fr/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Étiquette", - "unlabeled": "Sans étiquette" + "unlabeled": "Sans étiquette", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Reconnaître les visages sans étiquette" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Reconnaître les visages sans étiquette", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Photos" diff --git a/ui/extractedTranslations/it/translation.json b/ui/extractedTranslations/it/translation.json index 34b3c62e..05cd7089 100644 --- a/ui/extractedTranslations/it/translation.json +++ b/ui/extractedTranslations/it/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Etichetta", - "unlabeled": "Senza etichetta" + "unlabeled": "Senza etichetta", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Identifica facce senza etichetta" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Identifica facce senza etichetta", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Foto" diff --git a/ui/extractedTranslations/pl/translation.json b/ui/extractedTranslations/pl/translation.json index 96d9d103..83ec26d1 100644 --- a/ui/extractedTranslations/pl/translation.json +++ b/ui/extractedTranslations/pl/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Etykieta", - "unlabeled": "Nieoznakowany" + "unlabeled": "Nieoznakowany", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Zdjęcia" diff --git a/ui/extractedTranslations/sv/translation.json b/ui/extractedTranslations/sv/translation.json index aab3904d..ce1a52c5 100644 --- a/ui/extractedTranslations/sv/translation.json +++ b/ui/extractedTranslations/sv/translation.json @@ -68,10 +68,58 @@ }, "people_page": { "face_group": { + "action": { + "add_label": null, + "change_label": null, + "detach_face": null, + "merge_face": null, + "move_faces": null + }, "label_placeholder": "Märkning", - "unlabeled": "Omärkt" + "unlabeled": "Omärkt", + "unlabeled_person": null }, - "recognize_unlabeled_faces_button": "Känna igen omärkta ansikten" + "modal": { + "action": { + "merge": null + }, + "detach_image_faces": { + "action": { + "detach": null, + "select_images": null + }, + "description": null, + "title": null + }, + "merge_face_groups": { + "description": null, + "destination_table": { + "title": null + }, + "title": null + }, + "move_image_faces": { + "description": null, + "destination_face_group_table": { + "move_action": null, + "title": null + }, + "image_select_table": { + "next_action": null, + "title": null + }, + "title": null + } + }, + "recognize_unlabeled_faces_button": "Känna igen omärkta ansikten", + "table": { + "select_face_group": { + "search_faces_placeholder": null + }, + "select_image_faces": { + "search_images_placeholder": null + } + } }, "photos_page": { "title": "Bilder" diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.js b/ui/src/Pages/PeoplePage/FaceCircleImage.js deleted file mode 100644 index d77dc148..00000000 --- a/ui/src/Pages/PeoplePage/FaceCircleImage.js +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import styled from 'styled-components' -import { ProtectedImage } from '../../components/photoGallery/ProtectedMedia' - -const FaceImage = styled(ProtectedImage)` - position: absolute; - transform-origin: ${({ $origin }) => - `${$origin.x * 100}% ${$origin.y * 100}%`}; - object-fit: cover; - - transition: transform 250ms ease-out; -` - -const FaceImagePortrait = styled(FaceImage)` - width: 100%; - top: 50%; - transform: translateY(-50%) - ${({ $origin, $scale }) => - `translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.8, 1)})`}; - - ${({ $selectable, $origin, $scale }) => - $selectable - ? ` - &:hover { - transform: translateY(-50%) translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.85, 1)}) - ` - : ''} -` - -const FaceImageLandscape = styled(FaceImage)` - height: 100%; - left: 50%; - transform: translateX(-50%) - ${({ $origin, $scale }) => - `translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.8, 1)})`}; - - ${({ $selectable, $origin, $scale }) => - $selectable - ? ` - &:hover { - transform: translateX(-50%) translate(${(0.5 - $origin.x) * 100}%, ${ - (0.5 - $origin.y) * 100 - }%) scale(${Math.max($scale * 0.85, 1)}) - ` - : ''} -` - -const CircleImageWrapper = styled.div` - background-color: #eee; - position: relative; - border-radius: 50%; - width: ${({ size }) => size}; - height: ${({ size }) => size}; - object-fit: fill; - overflow: hidden; -` - -const FaceCircleImage = ({ imageFace, selectable, size = '150px' }) => { - if (!imageFace) { - return null - } - - const rect = imageFace.rectangle - - let scale = Math.min(1 / (rect.maxX - rect.minX), 1 / (rect.maxY - rect.minY)) - - let origin = { - x: (rect.minX + rect.maxX) / 2, - y: (rect.minY + rect.maxY) / 2, - } - - const SpecificFaceImage = - imageFace.media.thumbnail.width > imageFace.media.thumbnail.height - ? FaceImageLandscape - : FaceImagePortrait - return ( - - - - ) -} - -FaceCircleImage.propTypes = { - imageFace: PropTypes.object, - selectable: PropTypes.bool, - size: PropTypes.string, -} - -export default FaceCircleImage diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx new file mode 100644 index 00000000..d82ffc17 --- /dev/null +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.test.tsx @@ -0,0 +1,41 @@ +import '@testing-library/jest-dom' + +import React from 'react' +import { render, screen } from '@testing-library/react' +import FaceCircleImage from './FaceCircleImage' +import { myFaces_myFaceGroups_imageFaces } from './__generated__/myFaces' + +require('../../localization').setupLocalization() + +test('face circle image', () => { + const imageFace: myFaces_myFaceGroups_imageFaces = { + __typename: 'ImageFace', + id: '3', + media: { + id: '1', + __typename: 'Media', + title: 'my_image.jpg', + thumbnail: { + __typename: 'MediaURL', + url: 'http://localhost:4001/photo/thumbnail_my_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + }, + }, + rectangle: { + __typename: 'FaceRectangle', + minX: 0.27, + maxX: 0.34, + minY: 0.76, + maxY: 0.88, + }, + } + + render() + + expect(screen.getByRole('img')).toBeInTheDocument() + expect(screen.getByRole('img')).toHaveAttribute( + 'src', + imageFace.media.thumbnail!.url + ) +}) diff --git a/ui/src/Pages/PeoplePage/FaceCircleImage.tsx b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx new file mode 100644 index 00000000..8fc4b774 --- /dev/null +++ b/ui/src/Pages/PeoplePage/FaceCircleImage.tsx @@ -0,0 +1,125 @@ +import React from 'react' +import styled from 'styled-components' +import { ProtectedImage } from '../../components/photoGallery/ProtectedMedia' +import { + myFaces_myFaceGroups_imageFaces_media, + myFaces_myFaceGroups_imageFaces_rectangle, +} from './__generated__/myFaces' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const FaceImage = styled(({ origin, selectable, scale, ...rest }) => ( + +))<{ origin: { x: number; y: number }; selectable: boolean; scale: number }>` + position: absolute; + transform-origin: ${({ origin }) => `${origin.x * 100}% ${origin.y * 100}%`}; + object-fit: cover; + + transition: transform 250ms ease-out; +` + +const FaceImagePortrait = styled(FaceImage)` + width: 100%; + top: 50%; + transform: translateY(-50%) + ${({ origin, scale }) => + `translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.8, 1)})`}; + + ${({ selectable, origin, scale }) => + selectable + ? ` + &:hover { + transform: translateY(-50%) translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.85, 1)}) + ` + : ''} +` + +const FaceImageLandscape = styled(FaceImage)` + height: 100%; + left: 50%; + transform: translateX(-50%) + ${({ origin, scale }) => + `translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.8, 1)})`}; + + ${({ selectable, origin, scale }) => + selectable + ? ` + &:hover { + transform: translateX(-50%) translate(${(0.5 - origin.x) * 100}%, ${ + (0.5 - origin.y) * 100 + }%) scale(${Math.max(scale * 0.85, 1)}) + ` + : ''} +` + +const CircleImageWrapper = styled.div<{ size: string }>` + background-color: #eee; + position: relative; + border-radius: 50%; + width: ${({ size }) => size}; + height: ${({ size }) => size}; + object-fit: fill; + overflow: hidden; +` + +type FaceCircleImageFace = { + __typename: 'ImageFace' + id: string + rectangle: myFaces_myFaceGroups_imageFaces_rectangle + media: myFaces_myFaceGroups_imageFaces_media +} + +type FaceCircleImageProps = { + imageFace: FaceCircleImageFace + selectable: boolean + size?: string +} + +const FaceCircleImage = ({ + imageFace, + selectable, + size = '150px', +}: FaceCircleImageProps) => { + if (!imageFace) { + return null + } + + const rect = imageFace.rectangle + + const scale = Math.min( + 1 / (rect.maxX - rect.minX), + 1 / (rect.maxY - rect.minY) + ) + + const origin = { + x: (rect.minX + rect.maxX) / 2, + y: (rect.minY + rect.maxY) / 2, + } + + let SpecificFaceImage: typeof FaceImageLandscape | typeof FaceImagePortrait = + FaceImageLandscape + if (imageFace.media.thumbnail) { + SpecificFaceImage = + imageFace.media.thumbnail.width > imageFace.media.thumbnail.height + ? FaceImageLandscape + : FaceImagePortrait + } + + return ( + + + + ) +} + +export default FaceCircleImage diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx new file mode 100644 index 00000000..9cbb819a --- /dev/null +++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx @@ -0,0 +1,223 @@ +import '@testing-library/jest-dom' + +import React from 'react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import PeoplePage, { + FaceDetails, + MY_FACES_QUERY, + SET_GROUP_LABEL_MUTATION, +} from './PeoplePage' +import { MockedProvider } from '@apollo/client/testing' +import { MemoryRouter } from 'react-router' +import { myFaces_myFaceGroups } from './__generated__/myFaces' + +require('../../localization').setupLocalization() + +jest.mock('../../hooks/useScrollPagination', () => + jest.fn(() => ({ + finished: true, + containerElem: jest.fn(), + })) +) + +describe('PeoplePage component', () => { + const graphqlMocks = [ + { + request: { + query: MY_FACES_QUERY, + variables: { + limit: 50, + offset: 0, + }, + }, + result: { + data: { + myFaceGroups: [ + { + __typename: 'FaceGroup', + id: '3', + label: 'Person A', + imageFaceCount: 2, + imageFaces: [ + { + __typename: 'ImageFace', + id: '3', + rectangle: { + __typename: 'FaceRectangle', + minX: 0.2705079913139343, + maxX: 0.3408200144767761, + minY: 0.7691109776496887, + maxY: 0.881434977054596, + }, + media: { + __typename: 'Media', + id: '63', + title: 'image.jpg', + thumbnail: { + __typename: 'MediaURL', + url: 'http://localhost:4001/photo/thumbnail_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + }, + }, + }, + ], + }, + { + __typename: 'FaceGroup', + id: '1', + label: 'Person B', + imageFaceCount: 1, + imageFaces: [], + }, + ], + }, + }, + }, + ] + + test('people page', async () => { + const matchMock = { + params: { + person: undefined, + }, + } + + render( + + + + + + ) + + expect(screen.getByTestId('Layout')).toBeInTheDocument() + expect(screen.getByText('Recognize unlabeled faces')).toBeInTheDocument() + + await waitFor(() => { + expect(screen.getByText('Person A')).toBeInTheDocument() + expect(screen.getByText('Person B')).toBeInTheDocument() + }) + + expect( + screen + .getAllByRole('link') + .some(x => x.getAttribute('href') == '/people/1') + ).toBeTruthy() + + expect( + screen + .getAllByRole('link') + .some(x => x.getAttribute('href') == '/people/3') + ).toBeTruthy() + }) +}) + +describe('FaceDetails component', () => { + const faceGroup: myFaces_myFaceGroups = { + id: '3', + label: null, + imageFaceCount: 2, + imageFaces: [ + { + id: '3', + rectangle: { + minX: 0.2705079913139343, + maxX: 0.3408200144767761, + minY: 0.7691109776496887, + maxY: 0.881434977054596, + __typename: 'FaceRectangle', + }, + media: { + id: '63', + title: 'image.jpg', + thumbnail: { + url: 'http://localhost:4001/photo/thumbnail_image_jpg_p9x8dLWr.jpg', + width: 1024, + height: 641, + __typename: 'MediaURL', + }, + __typename: 'Media', + }, + __typename: 'ImageFace', + }, + ], + __typename: 'FaceGroup', + } + + test('unlabeled, no images', () => { + const emptyFaceGroup: myFaces_myFaceGroups = { + ...faceGroup, + imageFaces: [], + } + + render( + + + + ) + + expect(screen.getByText('Unlabeled')).toBeInTheDocument() + }) + + test('labeled, with thumbnail', () => { + const labeledFaceGroup: myFaces_myFaceGroups = { + ...faceGroup, + label: 'Some label', + } + + render( + + + + ) + + expect(screen.getByText(labeledFaceGroup.label!)).toBeInTheDocument() + expect(screen.queryByText('Unlabeled')).not.toBeInTheDocument() + }) + + test('add label to face group', async () => { + const graphqlMocks = [ + { + request: { + query: SET_GROUP_LABEL_MUTATION, + variables: { + groupID: '3', + label: 'John Doe', + }, + }, + newData: jest.fn(() => ({ + data: { + setFaceGroupLabel: { + __typename: 'FaceGroup', + id: '3', + label: 'John Doe', + }, + }, + })), + }, + ] + render( + + + + ) + + const btn = screen.getByRole('button') + expect(btn).toBeInTheDocument() + expect(screen.queryByRole('textbox')).not.toBeInTheDocument() + + fireEvent.click(btn) + + const input = screen.getByRole('textbox') + expect(input).toBeInTheDocument() + expect(input).toHaveValue('') + + fireEvent.change(input, { target: { value: 'John Doe' } }) + fireEvent.keyUp(input, { key: 'Enter', code: 'Enter' }) + + await waitFor(() => { + expect(graphqlMocks[0].newData).toHaveBeenCalled() + }) + }) +}) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.tsx similarity index 76% rename from ui/src/Pages/PeoplePage/PeoplePage.js rename to ui/src/Pages/PeoplePage/PeoplePage.tsx index 991447bd..0698b651 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.js +++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx @@ -1,5 +1,4 @@ import React, { createRef, useEffect, useState } from 'react' -import PropTypes from 'prop-types' import { gql, useMutation, useQuery } from '@apollo/client' import Layout from '../../Layout' import styled from 'styled-components' @@ -10,6 +9,16 @@ import FaceCircleImage from './FaceCircleImage' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' import { useTranslation } from 'react-i18next' +import { + setGroupLabel, + setGroupLabelVariables, +} from './__generated__/setGroupLabel' +import { + myFaces, + myFacesVariables, + myFaces_myFaceGroups, +} from './__generated__/myFaces' +import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces' export const MY_FACES_QUERY = gql` query myFaces($limit: Int, $offset: Int) { @@ -27,6 +36,7 @@ export const MY_FACES_QUERY = gql` } media { id + title thumbnail { url width @@ -55,7 +65,7 @@ const RECOGNIZE_UNLABELED_FACES_MUTATION = gql` } ` -const FaceDetailsButton = styled.button` +const FaceDetailsButton = styled.button<{ labeled: boolean }>` color: ${({ labeled }) => (labeled ? 'black' : '#aaa')}; width: 150px; margin: 12px auto 24px; @@ -73,13 +83,20 @@ const FaceDetailsButton = styled.button` const FaceLabel = styled.span`` -const FaceDetails = ({ group }) => { +type FaceDetailsProps = { + group: myFaces_myFaceGroups +} + +export const FaceDetails = ({ group }: FaceDetailsProps) => { const { t } = useTranslation() const [editLabel, setEditLabel] = useState(false) const [inputValue, setInputValue] = useState(group.label ?? '') - const inputRef = createRef() + const inputRef = createRef() - const [setGroupLabel, { loading }] = useMutation(SET_GROUP_LABEL_MUTATION, { + const [setGroupLabel, { loading }] = useMutation< + setGroupLabel, + setGroupLabelVariables + >(SET_GROUP_LABEL_MUTATION, { variables: { groupID: group.id, }, @@ -91,9 +108,7 @@ const FaceDetails = ({ group }) => { } useEffect(() => { - if (inputRef.current) { - inputRef.current.focus() - } + inputRef.current?.focus() }, [inputRef]) useEffect(() => { @@ -102,7 +117,7 @@ const FaceDetails = ({ group }) => { } }, [loading]) - const onKeyUp = e => { + const onKeyUp = (e: React.ChangeEvent & KeyboardEvent) => { if (e.key == 'Escape') { resetLabel() return @@ -111,6 +126,7 @@ const FaceDetails = ({ group }) => { if (e.key == 'Enter') { setGroupLabel({ variables: { + groupID: group.id, label: e.target.value == '' ? null : e.target.value, }, }) @@ -155,10 +171,6 @@ const FaceDetails = ({ group }) => { return label } -FaceDetails.propTypes = { - group: PropTypes.object.isRequired, -} - const FaceImagesCount = styled.span` background-color: #eee; color: #222; @@ -179,7 +191,11 @@ const EditIcon = styled(Icon)` } ` -const FaceGroup = ({ group }) => { +type FaceGroupProps = { + group: myFaces_myFaceGroups +} + +const FaceGroup = ({ group }: FaceGroupProps) => { const previewFace = group.imageFaces[0] return ( @@ -192,10 +208,6 @@ const FaceGroup = ({ group }) => { ) } -FaceGroup.propTypes = { - group: PropTypes.any, -} - const FaceGroupsWrapper = styled.div` display: flex; flex-wrap: wrap; @@ -204,27 +216,29 @@ const FaceGroupsWrapper = styled.div` const PeopleGallery = () => { const { t } = useTranslation() - const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, { + const { data, error, loading, fetchMore } = useQuery< + myFaces, + myFacesVariables + >(MY_FACES_QUERY, { variables: { limit: 50, offset: 0, }, }) - const [ - recognizeUnlabeled, - { loading: recognizeUnlabeledLoading }, - ] = useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION) + const [recognizeUnlabeled, { loading: recognizeUnlabeledLoading }] = + useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION) - const { containerElem, finished: finishedLoadingMore } = useScrollPagination({ - loading, - fetchMore, - data, - getItems: data => data.myFaceGroups, - }) + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.myFaceGroups, + }) if (error) { - return error.message + return
{error.message}
} let faces = null @@ -258,11 +272,20 @@ const PeopleGallery = () => { ) } -const PeoplePage = ({ match }) => { +type PeoplePageProps = { + match: { + params: { + person?: string + } + } +} + +const PeoplePage = ({ match }: PeoplePageProps) => { + const { t } = useTranslation() const faceGroup = match.params.person if (faceGroup) { return ( - + ) @@ -271,8 +294,4 @@ const PeoplePage = ({ match }) => { } } -PeoplePage.propTypes = { - match: PropTypes.object.isRequired, -} - export default PeoplePage diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx similarity index 52% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx index 7a53d872..d7701315 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal.tsx @@ -1,10 +1,23 @@ import { gql, useMutation } from '@apollo/client' -import PropTypes from 'prop-types' import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' import { Button, Modal } from 'semantic-ui-react' +import { isNil } from '../../../helpers/utils' import { MY_FACES_QUERY } from '../PeoplePage' +import { + myFaces_myFaceGroups, + myFaces_myFaceGroups_imageFaces, +} from '../__generated__/myFaces' import SelectImageFacesTable from './SelectImageFacesTable' +import { + detachImageFaces, + detachImageFacesVariables, +} from './__generated__/detachImageFaces' +import { + singleFaceGroup_faceGroup, + singleFaceGroup_faceGroup_imageFaces, +} from './__generated__/singleFaceGroup' const DETACH_IMAGE_FACES_MUTATION = gql` mutation detachImageFaces($faceIDs: [ID!]!) { @@ -15,12 +28,28 @@ const DETACH_IMAGE_FACES_MUTATION = gql` } ` -const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { - const [selectedImageFaces, setSelectedImageFaces] = useState([]) - let history = useHistory() +type DetachImageFacesModalProps = { + open: boolean + setOpen(open: boolean): void + faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup +} - const [detachImageFacesMutation] = useMutation(DETACH_IMAGE_FACES_MUTATION, { - variables: {}, +const DetachImageFacesModal = ({ + open, + setOpen, + faceGroup, +}: DetachImageFacesModalProps) => { + const { t } = useTranslation() + + const [selectedImageFaces, setSelectedImageFaces] = useState< + (myFaces_myFaceGroups_imageFaces | singleFaceGroup_faceGroup_imageFaces)[] + >([]) + const history = useHistory() + + const [detachImageFacesMutation] = useMutation< + detachImageFaces, + detachImageFacesVariables + >(DETACH_IMAGE_FACES_MUTATION, { refetchQueries: [ { query: MY_FACES_QUERY, @@ -44,6 +73,7 @@ const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { faceIDs, }, }).then(({ data }) => { + if (isNil(data)) throw new Error('Expected data not to be null') setOpen(false) history.push(`/people/${data.detachImageFaces.id}`) }) @@ -57,18 +87,25 @@ const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { onOpen={() => setOpen(true)} open={open} > - Detach Image Faces + + {t('people_page.modal.detach_image_faces.title', 'Detach Image Faces')} +

- Detach selected images of this face group and move them to a new - face group + {t( + 'people_page.modal.detach_image_faces.description', + 'Detach selected images of this face group and move them to a new face groups' + )}

@@ -76,7 +113,10 @@ const DetachImageFacesModal = ({ open, setOpen, faceGroup }) => { - + + {positiveButton} ) } -MoveImageFacesModal.propTypes = { - open: PropTypes.bool.isRequired, - setOpen: PropTypes.func.isRequired, - faceGroup: PropTypes.object, -} - export default MoveImageFacesModal diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx similarity index 64% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx index 13e869d0..d7ef5fb1 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectFaceGroupTable.tsx @@ -1,10 +1,12 @@ -import PropTypes from 'prop-types' import React, { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' import { Input, Pagination, Table } from 'semantic-ui-react' import styled from 'styled-components' import FaceCircleImage from '../FaceCircleImage' +import { myFaces_myFaceGroups } from '../__generated__/myFaces' +import { singleFaceGroup_faceGroup } from './__generated__/singleFaceGroup' -const FaceCircleWrapper = styled.div` +const FaceCircleWrapper = styled.div<{ $selected: boolean }>` display: inline-block; border-radius: 50%; border: 2px solid @@ -16,17 +18,31 @@ const FlexCell = styled(Table.Cell)` align-items: center; ` -export const RowLabel = styled.span` +export const RowLabel = styled.span<{ $selected: boolean }>` ${({ $selected }) => $selected && `font-weight: bold;`} margin-left: 12px; ` -const FaceGroupRow = ({ faceGroup, faceSelected, setFaceSelected }) => { +type FaceGroupRowProps = { + faceGroup: myFaces_myFaceGroups + faceSelected: boolean + setFaceSelected(): void +} + +const FaceGroupRow = ({ + faceGroup, + faceSelected, + setFaceSelected, +}: FaceGroupRowProps) => { return ( - + {faceGroup.label} @@ -34,10 +50,15 @@ const FaceGroupRow = ({ faceGroup, faceSelected, setFaceSelected }) => { ) } -FaceGroupRow.propTypes = { - faceGroup: PropTypes.object.isRequired, - faceSelected: PropTypes.bool.isRequired, - setFaceSelected: PropTypes.func.isRequired, +type SelectFaceGroupTableProps = { + faceGroups: myFaces_myFaceGroups[] + selectedFaceGroup: singleFaceGroup_faceGroup | myFaces_myFaceGroups | null + setSelectedFaceGroup: React.Dispatch< + React.SetStateAction< + singleFaceGroup_faceGroup | myFaces_myFaceGroups | null + > + > + title: string } const SelectFaceGroupTable = ({ @@ -45,7 +66,9 @@ const SelectFaceGroupTable = ({ selectedFaceGroup, setSelectedFaceGroup, title, -}) => { +}: SelectFaceGroupTableProps) => { + const { t } = useTranslation() + const PAGE_SIZE = 6 const [page, setPage] = useState(0) @@ -65,7 +88,7 @@ const SelectFaceGroupTable = ({ setSelectedFaceGroup(face)} /> )) @@ -86,7 +109,10 @@ const SelectFaceGroupTable = ({ value={searchValue} onChange={e => setSearchValue(e.target.value)} icon="search" - placeholder="Search faces..." + placeholder={t( + 'people_page.table.select_face_group.search_faces_placeholder', + 'Search faces...' + )} fluid /> @@ -105,7 +131,11 @@ const SelectFaceGroupTable = ({ activePage={page + 1} totalPages={Math.ceil(rows.length / PAGE_SIZE)} onPageChange={(_, { activePage }) => { - setPage(Math.ceil(activePage) - 1) + if (activePage) { + setPage(Math.ceil(activePage as number) - 1) + } else { + setPage(0) + } }} /> @@ -115,11 +145,4 @@ const SelectFaceGroupTable = ({ ) } -SelectFaceGroupTable.propTypes = { - faceGroups: PropTypes.array, - selectedFaceGroup: PropTypes.object, - setSelectedFaceGroup: PropTypes.func.isRequired, - title: PropTypes.string, -} - export default SelectFaceGroupTable diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx similarity index 68% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx index 4f476302..b33b057f 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SelectImageFacesTable.tsx @@ -1,16 +1,30 @@ -import PropTypes from 'prop-types' import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Checkbox, Input, Pagination, Table } from 'semantic-ui-react' import styled from 'styled-components' import { ProtectedImage } from '../../../components/photoGallery/ProtectedMedia' +import { myFaces_myFaceGroups_imageFaces } from '../__generated__/myFaces' import { RowLabel } from './SelectFaceGroupTable' +import { singleFaceGroup_faceGroup_imageFaces } from './__generated__/singleFaceGroup' const SelectImagePreview = styled(ProtectedImage)` max-width: 120px; max-height: 80px; ` -const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => { +type ImageFaceRowProps = { + imageFace: + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + faceSelected: boolean + setFaceSelected(): void +} + +const ImageFaceRow = ({ + imageFace, + faceSelected, + setFaceSelected, +}: ImageFaceRowProps) => { return ( @@ -18,7 +32,7 @@ const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => { @@ -31,10 +45,21 @@ const ImageFaceRow = ({ imageFace, faceSelected, setFaceSelected }) => { ) } -ImageFaceRow.propTypes = { - imageFace: PropTypes.object.isRequired, - faceSelected: PropTypes.bool.isRequired, - setFaceSelected: PropTypes.func.isRequired, +type SelectImageFacesTable = { + imageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + selectedImageFaces: ( + | myFaces_myFaceGroups_imageFaces + | singleFaceGroup_faceGroup_imageFaces + )[] + setSelectedImageFaces: React.Dispatch< + React.SetStateAction< + (myFaces_myFaceGroups_imageFaces | singleFaceGroup_faceGroup_imageFaces)[] + > + > + title: string } const SelectImageFacesTable = ({ @@ -42,7 +67,9 @@ const SelectImageFacesTable = ({ selectedImageFaces, setSelectedImageFaces, title, -}) => { +}: SelectImageFacesTable) => { + const { t } = useTranslation() + const PAGE_SIZE = 6 const [page, setPage] = useState(0) @@ -91,7 +118,10 @@ const SelectImageFacesTable = ({ value={searchValue} onChange={e => setSearchValue(e.target.value)} icon="search" - placeholder="Search images..." + placeholder={t( + 'people_page.table.select_image_faces.search_images_placeholder', + 'Search images...' + )} fluid /> @@ -110,7 +140,11 @@ const SelectImageFacesTable = ({ activePage={page + 1} totalPages={Math.ceil(rows.length / PAGE_SIZE)} onPageChange={(_, { activePage }) => { - setPage(Math.ceil(activePage) - 1) + if (activePage) { + setPage(Math.ceil(activePage as number) - 1) + } else { + setPage(0) + } }} /> @@ -120,11 +154,4 @@ const SelectImageFacesTable = ({ ) } -SelectImageFacesTable.propTypes = { - imageFaces: PropTypes.array, - selectedImageFaces: PropTypes.array, - setSelectedImageFaces: PropTypes.func.isRequired, - title: PropTypes.string, -} - export default SelectImageFacesTable diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx similarity index 51% rename from ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js rename to ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx index 60ab14cd..73270402 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.tsx @@ -1,10 +1,15 @@ import { gql, useQuery } from '@apollo/client' -import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useEffect, useReducer } from 'react' +import { useTranslation } from 'react-i18next' import PaginateLoader from '../../../components/PaginateLoader' import PhotoGallery from '../../../components/photoGallery/PhotoGallery' +import { photoGalleryReducer } from '../../../components/photoGallery/photoGalleryReducer' import useScrollPagination from '../../../hooks/useScrollPagination' import FaceGroupTitle from './FaceGroupTitle' +import { + singleFaceGroup, + singleFaceGroupVariables, +} from './__generated__/singleFaceGroup' export const SINGLE_FACE_GROUP = gql` query singleFaceGroup($id: ID!, $limit: Int!, $offset: Int!) { @@ -38,69 +43,65 @@ export const SINGLE_FACE_GROUP = gql` } ` -const SingleFaceGroup = ({ faceGroupID }) => { - const { data, error, loading, fetchMore } = useQuery(SINGLE_FACE_GROUP, { +type SingleFaceGroupProps = { + faceGroupID: string +} + +const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => { + const { t } = useTranslation() + + const { data, error, loading, fetchMore } = useQuery< + singleFaceGroup, + singleFaceGroupVariables + >(SINGLE_FACE_GROUP, { variables: { limit: 200, offset: 0, id: faceGroupID, }, }) - const [presenting, setPresenting] = useState(false) - const [activeIndex, setActiveIndex] = useState(-1) - const { containerElem, finished: finishedLoadingMore } = useScrollPagination({ - loading, - fetchMore, - data, - getItems: data => data.faceGroup.imageFaces, + const [mediaState, dispatchMedia] = useReducer(photoGalleryReducer, { + presenting: false, + activeIndex: -1, + media: [], }) + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.faceGroup.imageFaces, + }) + + useEffect(() => { + const media = data?.faceGroup.imageFaces.map(x => x.media) || [] + dispatchMedia({ type: 'replaceMedia', media }) + }, [data]) + const faceGroup = data?.faceGroup if (error) { return
{error.message}
} - let mediaGallery = null - if (faceGroup) { - const media = faceGroup.imageFaces.map(x => x.media) - - const nextImage = () => - setActiveIndex(i => Math.min(i + 1, media.length - 1)) - - const previousImage = () => setActiveIndex(i => Math.max(i - 1, 0)) - - mediaGallery = ( -
- - -
- ) - } - return (
- {mediaGallery} +
+ + +
) } -SingleFaceGroup.propTypes = { - faceGroupID: PropTypes.string, -} - export default SingleFaceGroup diff --git a/ui/src/Pages/PeoplePage/__generated__/myFaces.ts b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts index b56b4ac7..b947e437 100644 --- a/ui/src/Pages/PeoplePage/__generated__/myFaces.ts +++ b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts @@ -34,6 +34,7 @@ export interface myFaces_myFaceGroups_imageFaces_media_thumbnail { export interface myFaces_myFaceGroups_imageFaces_media { __typename: 'Media' id: string + title: string /** * URL to display the media in a smaller resolution */ diff --git a/ui/src/Pages/SharePage/SharePage.test.js b/ui/src/Pages/SharePage/SharePage.test.js index bd857581..1dbf8d9e 100644 --- a/ui/src/Pages/SharePage/SharePage.test.js +++ b/ui/src/Pages/SharePage/SharePage.test.js @@ -110,7 +110,7 @@ describe('load correct share page, based on graphql query', () => { expect(screen.getByText('Loading...')).toBeInTheDocument() - await waitForElementToBeRemoved(() => screen.getByText('Loading...')) + await waitForElementToBeRemoved(() => screen.queryByText('Loading...')) expect(screen.getByTestId('Layout')).toBeInTheDocument() expect(screen.getByTestId('MediaSharePage')).toBeInTheDocument() diff --git a/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts index 31c6b5ef..c3186684 100644 --- a/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts +++ b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts @@ -3,178 +3,181 @@ // @generated // This file was automatically generated and should not be edited. -import { MediaType } from "./../../../../__generated__/globalTypes"; +import { + OrderDirection, + MediaType, +} from './../../../../__generated__/globalTypes' // ==================================================== // GraphQL query operation: shareAlbumQuery // ==================================================== export interface shareAlbumQuery_album_subAlbums_thumbnail_thumbnail { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string } export interface shareAlbumQuery_album_subAlbums_thumbnail { - __typename: "Media"; + __typename: 'Media' /** * URL to display the media in a smaller resolution */ - thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null; + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null } export interface shareAlbumQuery_album_subAlbums { - __typename: "Album"; - id: string; - title: string; + __typename: 'Album' + id: string + title: string /** * An image in this album used for previewing this album */ - thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null; + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null } export interface shareAlbumQuery_album_media_thumbnail { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string /** * Width of the image in pixels */ - width: number; + width: number /** * Height of the image in pixels */ - height: number; + height: number } export interface shareAlbumQuery_album_media_downloads_mediaUrl { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string /** * Width of the image in pixels */ - width: number; + width: number /** * Height of the image in pixels */ - height: number; + height: number /** * The file size of the resource in bytes */ - fileSize: number; + fileSize: number } export interface shareAlbumQuery_album_media_downloads { - __typename: "MediaDownload"; - title: string; - mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl; + __typename: 'MediaDownload' + title: string + mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl } export interface shareAlbumQuery_album_media_highRes { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string /** * Width of the image in pixels */ - width: number; + width: number /** * Height of the image in pixels */ - height: number; + height: number } export interface shareAlbumQuery_album_media_videoWeb { - __typename: "MediaURL"; + __typename: 'MediaURL' /** * URL for previewing the image */ - url: string; + url: string } export interface shareAlbumQuery_album_media_exif { - __typename: "MediaEXIF"; + __typename: 'MediaEXIF' /** * The model name of the camera */ - camera: string | null; + camera: string | null /** * The maker of the camera */ - maker: string | null; + maker: string | null /** * The name of the lens */ - lens: string | null; - dateShot: any | null; + lens: string | null + dateShot: any | null /** * The exposure time of the image */ - exposure: number | null; + exposure: number | null /** * The aperature stops of the image */ - aperture: number | null; + aperture: number | null /** * The ISO setting of the image */ - iso: number | null; + iso: number | null /** * The focal length of the lens, when the image was taken */ - focalLength: number | null; + focalLength: number | null /** * A formatted description of the flash settings, when the image was taken */ - flash: number | null; + flash: number | null /** * An index describing the mode for adjusting the exposure of the image */ - exposureProgram: number | null; + exposureProgram: number | null } export interface shareAlbumQuery_album_media { - __typename: "Media"; - id: string; - title: string; - type: MediaType; + __typename: 'Media' + id: string + title: string + type: MediaType /** * URL to display the media in a smaller resolution */ - thumbnail: shareAlbumQuery_album_media_thumbnail | null; - downloads: shareAlbumQuery_album_media_downloads[]; + thumbnail: shareAlbumQuery_album_media_thumbnail | null + downloads: shareAlbumQuery_album_media_downloads[] /** * URL to display the photo in full resolution, will be null for videos */ - highRes: shareAlbumQuery_album_media_highRes | null; + highRes: shareAlbumQuery_album_media_highRes | null /** * URL to get the video in a web format that can be played in the browser, will be null for photos */ - videoWeb: shareAlbumQuery_album_media_videoWeb | null; - exif: shareAlbumQuery_album_media_exif | null; + videoWeb: shareAlbumQuery_album_media_videoWeb | null + exif: shareAlbumQuery_album_media_exif | null } export interface shareAlbumQuery_album { - __typename: "Album"; - id: string; - title: string; + __typename: 'Album' + id: string + title: string /** * The albums contained in this album */ - subAlbums: shareAlbumQuery_album_subAlbums[]; + subAlbums: shareAlbumQuery_album_subAlbums[] /** * The media inside this album */ - media: shareAlbumQuery_album_media[]; + media: shareAlbumQuery_album_media[] } export interface shareAlbumQuery { @@ -182,13 +185,15 @@ export interface shareAlbumQuery { * Get album by id, user must own the album or be admin * If valid tokenCredentials are provided, the album may be retrived without further authentication */ - album: shareAlbumQuery_album; + album: shareAlbumQuery_album } export interface shareAlbumQueryVariables { - id: string; - token: string; - password?: string | null; - limit?: number | null; - offset?: number | null; + id: string + token: string + password?: string | null + mediaOrderBy?: string | null + mediaOrderDirection?: OrderDirection | null + limit?: number | null + offset?: number | null }