From b34115cab0ffc215bdf8f3e5cac49f753bac9152 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Sat, 11 Jul 2020 16:42:27 +0200
Subject: [PATCH] Start on video integration with web ui

---
 api/graphql/resolvers/media.go                |  10 +-
 ui/src/Pages/AlbumPage/AlbumPage.js           |   4 +
 ui/src/Pages/PhotosPage/PhotosPage.js         |   4 +
 ui/src/Pages/SharePage/PhotoSharePage.js      |   4 +-
 .../components/photoGallery/PhotoGallery.js   |   6 +-
 .../photoGallery/presentView/PresentMedia.js  |  59 +++++++++
 .../presentView/PresentNavigationOverlay.js   |   3 +
 .../photoGallery/presentView/PresentPhoto.js  |  35 -----
 .../photoGallery/presentView/PresentView.js   |  13 +-
 .../{PhotoSidebar.js => MediaSidebar.js}      | 120 ++++++++++++------
 10 files changed, 169 insertions(+), 89 deletions(-)
 create mode 100644 ui/src/components/photoGallery/presentView/PresentMedia.js
 delete mode 100644 ui/src/components/photoGallery/presentView/PresentPhoto.js
 rename ui/src/components/sidebar/{PhotoSidebar.js => MediaSidebar.js} (55%)

diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go
index 9fa0f8d..f3d8308 100644
--- a/api/graphql/resolvers/media.go
+++ b/api/graphql/resolvers/media.go
@@ -101,6 +101,10 @@ func (r *mediaResolver) Downloads(ctx context.Context, obj *models.Media) ([]*mo
 			title = "Small"
 		case url.Purpose == models.PhotoHighRes:
 			title = "Large"
+		case url.Purpose == models.VideoThumbnail:
+			title = "Video thumbnail"
+		case url.Purpose == models.VideoWeb:
+			title = "Web optimized video"
 		}
 
 		downloads = append(downloads, &models.MediaDownload{
@@ -158,7 +162,11 @@ func (r *mediaResolver) VideoWeb(ctx context.Context, obj *models.Media) (*model
 
 	url, err := models.NewMediaURLFromRow(row)
 	if err != nil {
-		return nil, errors.Wrapf(err, "could not query video web-format url (%s)", obj.Path)
+		if err == sql.ErrNoRows {
+			return nil, nil
+		} else {
+			return nil, errors.Wrapf(err, "could not query video web-format url (%s)", obj.Path)
+		}
 	}
 
 	return url, nil
diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js
index f462062..6d959f2 100644
--- a/ui/src/Pages/AlbumPage/AlbumPage.js
+++ b/ui/src/Pages/AlbumPage/AlbumPage.js
@@ -20,6 +20,7 @@ const albumQuery = gql`
       }
       media(filter: { order_by: "title", order_direction: DESC }) {
         id
+        type
         thumbnail {
           url
           width
@@ -28,6 +29,9 @@ const albumQuery = gql`
         highRes {
           url
         }
+        videoWeb {
+          url
+        }
         favorite
       }
     }
diff --git a/ui/src/Pages/PhotosPage/PhotosPage.js b/ui/src/Pages/PhotosPage/PhotosPage.js
index 101146d..d77cfd7 100644
--- a/ui/src/Pages/PhotosPage/PhotosPage.js
+++ b/ui/src/Pages/PhotosPage/PhotosPage.js
@@ -15,6 +15,7 @@ const photoQuery = gql`
       ) {
         id
         title
+        type
         thumbnail {
           url
           width
@@ -23,6 +24,9 @@ const photoQuery = gql`
         highRes {
           url
         }
+        videoWeb {
+          url
+        }
         favorite
       }
     }
diff --git a/ui/src/Pages/SharePage/PhotoSharePage.js b/ui/src/Pages/SharePage/PhotoSharePage.js
index a143694..6a44aa5 100644
--- a/ui/src/Pages/SharePage/PhotoSharePage.js
+++ b/ui/src/Pages/SharePage/PhotoSharePage.js
@@ -4,7 +4,7 @@ import styled from 'styled-components'
 import Layout from '../../Layout'
 import ProtectedImage from '../../components/photoGallery/ProtectedImage'
 import { SidebarConsumer } from '../../components/sidebar/Sidebar'
-import PhotoSidebar from '../../components/sidebar/PhotoSidebar'
+import MediaSidebar from '../../components/sidebar/MediaSidebar'
 
 const DisplayPhoto = styled(ProtectedImage)`
   width: 100%;
@@ -22,7 +22,7 @@ const AlbumSharePage = ({ photo }) => {
             <DisplayPhoto
               src={photo.highRes.url}
               onLoad={() => {
-                updateSidebar(<PhotoSidebar photo={photo} hidePreview />)
+                updateSidebar(<MediaSidebar media={photo} hidePreview />)
               }}
             />
           </>
diff --git a/ui/src/components/photoGallery/PhotoGallery.js b/ui/src/components/photoGallery/PhotoGallery.js
index 0676823..7fa0a7e 100644
--- a/ui/src/components/photoGallery/PhotoGallery.js
+++ b/ui/src/components/photoGallery/PhotoGallery.js
@@ -5,7 +5,7 @@ import { Photo, PhotoThumbnail } from './Photo'
 import PresentView from './presentView/PresentView'
 import PropTypes from 'prop-types'
 import { SidebarConsumer } from '../sidebar/Sidebar'
-import PhotoSidebar from '../sidebar/PhotoSidebar'
+import MediaSidebar from '../sidebar/MediaSidebar'
 
 const Gallery = styled.div`
   display: flex;
@@ -79,7 +79,7 @@ const PhotoGallery = ({
             key={photo.id}
             photo={photo}
             onSelectImage={index => {
-              updateSidebar(<PhotoSidebar photo={photo} />)
+              updateSidebar(<MediaSidebar media={photo} />)
               onSelectImage(index)
             }}
             setPresenting={setPresenting}
@@ -109,7 +109,7 @@ const PhotoGallery = ({
           </Gallery>
           {presenting && (
             <PresentView
-              photo={activeImage}
+              media={activeImage}
               {...{ nextImage, previousImage, setPresenting }}
             />
           )}
diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.js b/ui/src/components/photoGallery/presentView/PresentMedia.js
new file mode 100644
index 0000000..e1947c5
--- /dev/null
+++ b/ui/src/components/photoGallery/presentView/PresentMedia.js
@@ -0,0 +1,59 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import styled from 'styled-components'
+import ProtectedImage from '../ProtectedImage'
+
+const StyledPhoto = styled(ProtectedImage)`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  object-fit: contain;
+  object-position: center;
+`
+
+const StyledVideo = styled.video`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+`
+
+const PresentMedia = ({ media, imageLoaded, ...otherProps }) => {
+  if (media.type == 'photo') {
+    return (
+      <div {...otherProps}>
+        <StyledPhoto src={media.thumbnail.url} />
+        <StyledPhoto
+          style={{ display: 'none' }}
+          src={media.highRes.url}
+          onLoad={e => {
+            e.target.style.display = 'initial'
+            imageLoaded && imageLoaded()
+          }}
+        />
+      </div>
+    )
+  }
+
+  if (media.type == 'video') {
+    return (
+      <div {...otherProps}>
+        <StyledVideo controls key={media.id}>
+          <source src={media.videoWeb.url} type="video/mp4" />
+        </StyledVideo>
+      </div>
+    )
+  }
+
+  throw new Error(`Unknown media type '${media.type}'`)
+}
+
+PresentMedia.propTypes = {
+  media: PropTypes.object.isRequired,
+  imageLoaded: PropTypes.func,
+}
+
+export default PresentMedia
diff --git a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.js b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.js
index cc41602..e4d9a08 100644
--- a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.js
+++ b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.js
@@ -64,6 +64,7 @@ const NavigationButton = styled(OverlayButton)`
 `
 
 const PresentNavigationOverlay = ({
+  children,
   nextImage,
   previousImage,
   setPresenting,
@@ -92,6 +93,7 @@ const PresentNavigationOverlay = ({
         onMouseMove.current()
       }}
     >
+      {children}
       <NavigationButton
         className={hide && 'hide'}
         float="left"
@@ -117,6 +119,7 @@ const PresentNavigationOverlay = ({
 }
 
 PresentNavigationOverlay.propTypes = {
+  children: PropTypes.element,
   nextImage: PropTypes.func.isRequired,
   previousImage: PropTypes.func.isRequired,
   setPresenting: PropTypes.func.isRequired,
diff --git a/ui/src/components/photoGallery/presentView/PresentPhoto.js b/ui/src/components/photoGallery/presentView/PresentPhoto.js
deleted file mode 100644
index 66e5288..0000000
--- a/ui/src/components/photoGallery/presentView/PresentPhoto.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import styled from 'styled-components'
-import ProtectedImage from '../ProtectedImage'
-
-const StyledPhoto = styled(ProtectedImage)`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100vw;
-  height: 100vh;
-  object-fit: contain;
-  object-position: center;
-`
-
-const PresentPhoto = ({ photo, imageLoaded, ...otherProps }) => (
-  <div {...otherProps}>
-    <StyledPhoto src={photo.thumbnail.url} />
-    <StyledPhoto
-      style={{ display: 'none' }}
-      src={photo.highRes.url}
-      onLoad={e => {
-        e.target.style.display = 'initial'
-        imageLoaded && imageLoaded()
-      }}
-    />
-  </div>
-)
-
-PresentPhoto.propTypes = {
-  photo: PropTypes.object.isRequired,
-  imageLoaded: PropTypes.func,
-}
-
-export default PresentPhoto
diff --git a/ui/src/components/photoGallery/presentView/PresentView.js b/ui/src/components/photoGallery/presentView/PresentView.js
index 448de33..dacb8c4 100644
--- a/ui/src/components/photoGallery/presentView/PresentView.js
+++ b/ui/src/components/photoGallery/presentView/PresentView.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types'
 import React from 'react'
 import styled, { createGlobalStyle } from 'styled-components'
 import PresentNavigationOverlay from './PresentNavigationOverlay'
-import PresentPhoto from './PresentPhoto'
+import PresentMedia from './PresentMedia'
 
 const StyledContainer = styled.div`
   position: fixed;
@@ -23,7 +23,7 @@ const PreventScroll = createGlobalStyle`
 
 const PresentView = ({
   className,
-  photo,
+  media,
   imageLoaded,
   nextImage,
   previousImage,
@@ -31,15 +31,14 @@ const PresentView = ({
 }) => (
   <StyledContainer {...className}>
     <PreventScroll />
-    <PresentPhoto photo={photo} imageLoaded={imageLoaded} />
-    <PresentNavigationOverlay
-      {...{ nextImage, previousImage, setPresenting }}
-    />
+    <PresentNavigationOverlay {...{ nextImage, previousImage, setPresenting }}>
+      <PresentMedia media={media} imageLoaded={imageLoaded} />
+    </PresentNavigationOverlay>
   </StyledContainer>
 )
 
 PresentView.propTypes = {
-  photo: PropTypes.object.isRequired,
+  media: PropTypes.object.isRequired,
   imageLoaded: PropTypes.func,
   className: PropTypes.string,
   nextImage: PropTypes.func.isRequired,
diff --git a/ui/src/components/sidebar/PhotoSidebar.js b/ui/src/components/sidebar/MediaSidebar.js
similarity index 55%
rename from ui/src/components/sidebar/PhotoSidebar.js
rename to ui/src/components/sidebar/MediaSidebar.js
index 814e1c0..e7b06a9 100644
--- a/ui/src/components/sidebar/PhotoSidebar.js
+++ b/ui/src/components/sidebar/MediaSidebar.js
@@ -1,23 +1,34 @@
-import React, { Component } from 'react'
+import React, { Component, useEffect } from 'react'
 import PropTypes from 'prop-types'
 import styled from 'styled-components'
-import { Query } from 'react-apollo'
+import { useLazyQuery } from 'react-apollo'
 import gql from 'graphql-tag'
 import SidebarItem from './SidebarItem'
 import ProtectedImage from '../photoGallery/ProtectedImage'
 import SidebarShare from './Sharing'
 import SidebarDownload from './SidebarDownload'
 
-const photoQuery = gql`
+const mediaQuery = gql`
   query sidebarPhoto($id: Int!) {
     media(id: $id) {
       id
       title
+      type
       highRes {
         url
         width
         height
       }
+      thumbnail {
+        url
+        width
+        height
+      }
+      videoWeb {
+        url
+        width
+        height
+      }
       exif {
         camera
         maker
@@ -50,6 +61,35 @@ const PreviewImage = styled(ProtectedImage)`
   object-fit: contain;
 `
 
+const PreviewVideo = styled.video`
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+`
+
+const PreviewMedia = ({ media, previewImage }) => {
+  if (media.type == null || media.type == 'photo') {
+    return <PreviewImage src={previewImage.url} />
+  }
+
+  if (media.type == 'video') {
+    return (
+      <PreviewVideo controls key={media.id}>
+        <source src={media.videoWeb.url} type="video/mp4" />
+      </PreviewVideo>
+    )
+  }
+
+  throw new Error('Unknown media type')
+}
+
+PreviewMedia.propTypes = {
+  media: PropTypes.object.isRequired,
+  previewImage: PropTypes.object.isRequired,
+}
+
 const Name = styled.div`
   text-align: center;
   font-size: 1.2rem;
@@ -85,18 +125,18 @@ const exposurePrograms = {
   '8': 'Landscape mode ',
 }
 
-const SidebarContent = ({ photo, hidePreview }) => {
+const SidebarContent = ({ media, hidePreview }) => {
   let exifItems = []
 
-  if (photo && photo.exif) {
+  if (media && media.exif) {
     let exifKeys = Object.keys(exifNameLookup).filter(
-      x => !!photo.exif[x] && x != '__typename'
+      x => !!media.exif[x] && x != '__typename'
     )
 
     let exif = exifKeys.reduce(
       (prev, curr) => ({
         ...prev,
-        [curr]: photo.exif[curr],
+        [curr]: media.exif[curr],
       }),
       {}
     )
@@ -120,9 +160,9 @@ const SidebarContent = ({ photo, hidePreview }) => {
   }
 
   let previewImage = null
-  if (photo) {
-    if (photo.highRes) previewImage = photo.highRes
-    else if (photo.thumbnail) previewImage = photo.thumbnail
+  if (media) {
+    if (media.highRes) previewImage = media.highRes
+    else if (media.thumbnail) previewImage = media.thumbnail
   }
 
   return (
@@ -131,55 +171,53 @@ const SidebarContent = ({ photo, hidePreview }) => {
         <PreviewImageWrapper
           imageAspect={previewImage.height / previewImage.width}
         >
-          <PreviewImage src={previewImage.url} />
+          <PreviewMedia previewImage={previewImage} media={media} />
         </PreviewImageWrapper>
       )}
-      <Name>{photo && photo.title}</Name>
+      <Name>{media && media.title}</Name>
       <ExifInfo>{exifItems}</ExifInfo>
-      <SidebarDownload photo={photo} />
-      <SidebarShare photo={photo} />
+      <SidebarDownload photo={media} />
+      <SidebarShare photo={media} />
     </div>
   )
 }
 
 SidebarContent.propTypes = {
-  photo: PropTypes.object,
+  media: PropTypes.object,
   hidePreview: PropTypes.bool,
 }
 
-class PhotoSidebar extends Component {
-  render() {
-    const { photo, hidePreview } = this.props
+const MediaSidebar = ({ media, hidePreview }) => {
+  const [loadMedia, { loading, error, data }] = useLazyQuery(mediaQuery)
 
-    if (!photo) return null
-
-    if (!localStorage.getItem('token')) {
-      return <SidebarContent photo={photo} hidePreview={hidePreview} />
+  useEffect(() => {
+    if (media != null && localStorage.getItem('token')) {
+      loadMedia({
+        variables: {
+          id: media.id,
+        },
+      })
     }
+  }, [media])
 
-    return (
-      <div>
-        <Query query={photoQuery} variables={{ id: photo.id }}>
-          {({ loading, error, data }) => {
-            if (error) return error
+  if (!media) return null
 
-            if (loading) {
-              return <SidebarContent photo={photo} hidePreview={hidePreview} />
-            }
-
-            return (
-              <SidebarContent photo={data.media} hidePreview={hidePreview} />
-            )
-          }}
-        </Query>
-      </div>
-    )
+  if (!localStorage.getItem('token')) {
+    return <SidebarContent media={media} hidePreview={hidePreview} />
   }
+
+  if (error) return error
+
+  if (loading || data == null) {
+    return <SidebarContent media={media} hidePreview={hidePreview} />
+  }
+
+  return <SidebarContent media={data.media} hidePreview={hidePreview} />
 }
 
-PhotoSidebar.propTypes = {
-  photo: PropTypes.object.isRequired,
+MediaSidebar.propTypes = {
+  media: PropTypes.object.isRequired,
   hidePreview: PropTypes.bool,
 }
 
-export default PhotoSidebar
+export default MediaSidebar
