From fa13e4b3e04339067a8c70f5308772aa574b714f Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Sun, 1 Aug 2021 02:39:02 +0200
Subject: [PATCH] Implement improved EXIF support

---
 ui/src/App.css                          |   4 -
 ui/src/api/model/Image.ts               |  10 +-
 ui/src/components/ImageList.tsx         |  14 +-
 ui/src/components/ImageMetadataView.tsx | 196 +++++++++++++++++++++++
 ui/src/components/ImageView.tsx         | 202 ++++++++----------------
 5 files changed, 275 insertions(+), 151 deletions(-)
 create mode 100644 ui/src/components/ImageMetadataView.tsx

diff --git a/ui/src/App.css b/ui/src/App.css
index d31b352..e69de29 100644
--- a/ui/src/App.css
+++ b/ui/src/App.css
@@ -1,4 +0,0 @@
-img {
-    max-width: 200px;
-    max-height: 200px;
-}
diff --git a/ui/src/api/model/Image.ts b/ui/src/api/model/Image.ts
index 16683c3..710716b 100644
--- a/ui/src/api/model/Image.ts
+++ b/ui/src/api/model/Image.ts
@@ -7,9 +7,17 @@ export interface Image {
     updated_at: string,
     original_name: string,
     mime_type: string,
-    state: string,
+    state: ImageState,
     metadata: {
         [key: string]: string
     },
     url: string,
 }
+
+export enum ImageState {
+    CREATED = "created",
+    QUEUED = "queued",
+    IN_PROGRESS = "in_progress",
+    DONE = "done",
+    ERROR = "error"
+}
diff --git a/ui/src/components/ImageList.tsx b/ui/src/components/ImageList.tsx
index f7b025d..f46895f 100644
--- a/ui/src/components/ImageList.tsx
+++ b/ui/src/components/ImageList.tsx
@@ -9,14 +9,12 @@ export default function ImageList() {
         <div>
             <p>{status}</p>
             <p>{error as string}</p>
-            <ul>
-                {data?.map(image => (
-                    <ImageView
-                        key={image.id}
-                        image={image}
-                    />
-                ))}
-            </ul>
+            {data?.map(image => (
+                <ImageView
+                    key={image.id}
+                    image={image}
+                />
+            ))}
         </div>
     );
 }
diff --git a/ui/src/components/ImageMetadataView.tsx b/ui/src/components/ImageMetadataView.tsx
new file mode 100644
index 0000000..b7a2b9a
--- /dev/null
+++ b/ui/src/components/ImageMetadataView.tsx
@@ -0,0 +1,196 @@
+import React, {Fragment} from "react";
+import {ImageMetadata, ratioToTime} from "../metadata/ImageMetadata";
+import {ratioToFloat} from "../metadata/Ratio";
+import {ExposureMode} from "../metadata/ExposureMode";
+import {ExposureProgram} from "../metadata/ExposureProgram";
+import {LightSource} from "../metadata/LightSource";
+import {MeteringMode} from "../metadata/MeteringMode";
+import {WhiteBalance} from "../metadata/WhiteBalance";
+import {SceneMode} from "../metadata/SceneMode";
+import {ContrastProcessing} from "../metadata/ContrastProcessing";
+import {SharpnessProcessing} from "../metadata/SharpnessProcessing";
+import {SubjectDistanceRange} from "../metadata/SubjectDistanceRange";
+import {BrightnessMedium, Camera, Copyright, Event, Exposure, PhotoCamera, ZoomIn} from "@material-ui/icons";
+import {AngleAcute, ArrowExpandHorizontal, Blur, CameraTimer, Flash, WhiteBalanceIncandescent} from "mdi-material-ui";
+import {FlashMode} from "../metadata/FlashMode";
+import {ListItem, ListItemIcon, ListItemText} from "@material-ui/core";
+
+export interface ImageMetadataViewProps {
+    metadata: ImageMetadata
+}
+
+export default function ImageMetadataView({metadata}: ImageMetadataViewProps) {
+    return (
+        <Fragment>
+            {metadata.make !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><PhotoCamera/></ListItemIcon>
+                    <ListItemText primary="Make" secondary={metadata.make}/>
+                </ListItem>
+            )}
+            {metadata.model !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="Model" secondary={metadata.model}/>
+                </ListItem>
+            )}
+            {metadata.software !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="Software" secondary={metadata.software}/>
+                </ListItem>
+            )}
+            {metadata.copyright !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><Copyright/></ListItemIcon>
+                    <ListItemText primary="Copyright" secondary={metadata.copyright}/>
+                </ListItem>
+            )}
+            {metadata.dateTimeCreated !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><Event/></ListItemIcon>
+                    <ListItemText primary="Created At" secondary={metadata.dateTimeCreated.toISOString()}/>
+                </ListItem>
+            )}
+            {metadata.dateTimeDigitized !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="Digitized At"
+                                  secondary={metadata.dateTimeDigitized.toISOString()}/>
+                </ListItem>
+            )}
+            {metadata.dateTimeOriginal !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="Shot At" secondary={metadata.dateTimeOriginal.toISOString()}/>
+                </ListItem>
+            )}
+            {metadata.digitalZoomRatio !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><ZoomIn/></ListItemIcon>
+                    <ListItemText primary="Zoom" secondary={`${ratioToFloat(metadata.digitalZoomRatio)}x`}/>
+                </ListItem>
+            )}
+            {metadata.focalLength !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><AngleAcute/></ListItemIcon>
+                    <ListItemText primary="Focal Length" secondary={`${ratioToFloat(metadata.focalLength)}mm`}/>
+                </ListItem>
+            )}
+            {metadata.focalLength35mm !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="35mm equivalent"
+                                  secondary={`${ratioToFloat(metadata.focalLength35mm)}mm`}/>
+                </ListItem>
+            )}
+            {metadata.shutterSpeed !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><CameraTimer/></ListItemIcon>
+                    <ListItemText primary="Shutter Speed" secondary={ratioToTime(metadata.shutterSpeed)}/>
+                </ListItem>
+            )}
+            {metadata.aperture !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><Camera/></ListItemIcon>
+                    <ListItemText primary="Aperture" secondary={ratioToFloat(metadata.aperture)}/>
+                </ListItem>
+            )}
+            {metadata.isoSpeedRating !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon>ISO</ListItemIcon>
+                    <ListItemText primary="ISO" secondary={metadata.isoSpeedRating}/>
+                </ListItem>
+            )}
+            {metadata.exposure !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><Exposure/></ListItemIcon>
+                    <ListItemText primary="Exposure" secondary={ratioToFloat(metadata.exposure)}/>
+                </ListItem>
+            )}
+            {metadata.exposureMode !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="Mode" secondary={ExposureMode[metadata.exposureMode]}/>
+                </ListItem>
+            )}
+            {metadata.exposureProgram !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="Program" secondary={ExposureProgram[metadata.exposureProgram]}/>
+                </ListItem>
+            )}
+            {metadata.meteringMode !== undefined && (
+                <ListItem dense>
+                    <ListItemText inset primary="Metering mode" secondary={MeteringMode[metadata.meteringMode]}/>
+                </ListItem>
+            )}
+            {metadata.flash !== undefined && (
+                <Fragment>
+                    <ListItem dense>
+                        <ListItemIcon><Flash/></ListItemIcon>
+                        <ListItemText primary="Flash"
+                                      secondary={metadata.flash.available ? "Available" : "Unavailable"}/>
+                    </ListItem>
+                    <ListItem dense>
+                        <ListItemText inset primary="Fired" secondary={metadata.flash.fired ? "Yes" : "No"}/>
+                    </ListItem>
+                    <ListItem dense>
+                        <ListItemText inset primary="Red Eye Reduction"
+                                      secondary={metadata.flash.redEyeReduction ? "Yes" : "No"}/>
+                    </ListItem>
+                    {metadata.flash.mode !== undefined && (
+                        <ListItem dense>
+                            <ListItemText inset primary="Mode" secondary={FlashMode[metadata.flash.mode]}/>
+                        </ListItem>
+                    )}
+                    {metadata.flash.strength !== undefined && (
+                        <ListItem dense>
+                            <ListItemText inset primary="Strength" secondary={`${metadata.flash.strength} BCPS`}/>
+                        </ListItem>
+                    )}
+                    <ListItem dense>
+                        <ListItemText inset primary="Strobe Detection"
+                                      secondary={!metadata.flash.strobeDetection.available ? "Unvailable" :
+                                          metadata.flash.strobeDetection.detected ? "Strobe detected" :
+                                              "No strobe detected"}
+                        />
+                    </ListItem>
+                </Fragment>
+            )}
+            {metadata.lightSource !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><WhiteBalanceIncandescent/></ListItemIcon>
+                    <ListItemText primary="Light Source" secondary={LightSource[metadata.lightSource]}/>
+                </ListItem>
+            )}
+            {metadata.whiteBalance !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><WhiteBalanceIncandescent/></ListItemIcon>
+                    <ListItemText primary="White balance" secondary={WhiteBalance[metadata.whiteBalance]}/>
+                </ListItem>
+            )}
+            {metadata.subjectDistance !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><ArrowExpandHorizontal/></ListItemIcon>
+                    <ListItemText primary="Subject Distance" secondary={metadata.subjectDistance}/>
+                </ListItem>
+            )}
+            {metadata.subjectDistanceRange !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><ArrowExpandHorizontal/></ListItemIcon>
+                    <ListItemText primary="Subject Distance Range"
+                                  secondary={SubjectDistanceRange[metadata.subjectDistanceRange]}/>
+                </ListItem>
+            )}
+            {metadata.sceneMode !== undefined && (
+                <p><b>Scene Mode</b>: {SceneMode[metadata.sceneMode]}</p>
+            )}
+            {metadata.contrast !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><BrightnessMedium/></ListItemIcon>
+                    <ListItemText primary="Contrast Processing" secondary={ContrastProcessing[metadata.contrast]}/>
+                </ListItem>
+            )}
+            {metadata.sharpness !== undefined && (
+                <ListItem dense>
+                    <ListItemIcon><Blur/></ListItemIcon>
+                    <ListItemText primary="Sharpness Processing" secondary={SharpnessProcessing[metadata.sharpness]}/>
+                </ListItem>
+            )}
+        </Fragment>
+    );
+}
diff --git a/ui/src/components/ImageView.tsx b/ui/src/components/ImageView.tsx
index cf98f57..a8bc740 100644
--- a/ui/src/components/ImageView.tsx
+++ b/ui/src/components/ImageView.tsx
@@ -1,21 +1,12 @@
 import {Image} from "../api/model/Image";
-import React, {Fragment, useMemo, useState} from "react";
+import React, {useMemo, useState} from "react";
 import {useUpdateImage} from "../api/useUpdateImage";
 import {useDeleteImage} from "../api/useDeleteImage";
-import {parseMetadata, ratioToTime} from "../metadata/ImageMetadata";
-import {ratioToFloat} from "../metadata/Ratio";
-import {ExposureMode} from "../metadata/ExposureMode";
-import {ExposureProgram} from "../metadata/ExposureProgram";
-import {LightSource} from "../metadata/LightSource";
-import {MeteringMode} from "../metadata/MeteringMode";
-import {WhiteBalance} from "../metadata/WhiteBalance";
-import {SceneMode} from "../metadata/SceneMode";
-import {ContrastProcessing} from "../metadata/ContrastProcessing";
-import {SharpnessProcessing} from "../metadata/SharpnessProcessing";
-import {SubjectDistanceRange} from "../metadata/SubjectDistanceRange";
-import {BrightnessMedium, Camera, Copyright, Event, Exposure, PhotoCamera, ZoomIn} from "@material-ui/icons";
-import {AngleAcute, ArrowExpandHorizontal, Blur, CameraTimer, Flash, WhiteBalanceIncandescent} from "mdi-material-ui";
-import {FlashMode} from "../metadata/FlashMode";
+import {parseMetadata} from "../metadata/ImageMetadata";
+import ImageMetadataView from "./ImageMetadataView";
+import {Button, List, ListItem, ListItemIcon, ListItemText, TextField} from "@material-ui/core";
+import {Event, Info} from "@material-ui/icons";
+import {File, Tag} from "mdi-material-ui";
 
 export interface ImageProps {
     image: Image
@@ -37,135 +28,70 @@ export default function ImageView({image}: ImageProps) {
             <p>RemoveError: {JSON.stringify(removeError, null, 2)}</p>
             <p>UpdateLoading: {JSON.stringify(updateLoading, null, 2)}</p>
             <p>RemoveLoading: {JSON.stringify(removeLoading, null, 2)}</p>
-            <p>{image.id}</p>
-            <p>{image.owner}</p>
-            <label>
-                Title
-                <input
-                    type="text"
-                    value={title}
-                    onChange={({target: {value}}) =>
-                        setTitle(value)}
-                />
-            </label>
-            <br/>
-            <label>
-                Description
-                <input
-                    type="text"
-                    value={description}
-                    onChange={({target: {value}}) =>
-                        setDescription(value)}
-                />
-            </label>
-            <p>{image.original_name}</p>
-            <p>{image.mime_type}</p>
-            <p>{image.created_at}</p>
-            <p>{image.updated_at}</p>
-            <p>{image.state}</p>
-            <h3>Metadata</h3>
-            {metadata.make !== undefined && (
-                <p><b>Make</b>: {metadata.make}</p>
-            )}
-            {metadata.model !== undefined && (
-                <p><PhotoCamera/><b>Model</b>: {metadata.model}</p>
-            )}
-            {metadata.software !== undefined && (
-                <p><b>Software</b>: {metadata.software}</p>
-            )}
-            {metadata.copyright !== undefined && (
-                <p><Copyright/><b>Copyright</b>: {metadata.copyright}</p>
-            )}
-            {metadata.dateTimeCreated !== undefined && (
-                <p><Event/><b>DateTime Created</b>: {metadata.dateTimeCreated?.toISOString()}</p>
-            )}
-            {metadata.dateTimeDigitized !== undefined && (
-                <p><Event/><b>DateTime Digitized</b>: {metadata.dateTimeDigitized?.toISOString()}</p>
-            )}
-            {metadata.dateTimeOriginal !== undefined && (
-                <p><Event/><b>DateTime Original</b>: {metadata.dateTimeOriginal?.toISOString()}</p>
-            )}
-            {metadata.digitalZoomRatio !== undefined && (
-                <p><ZoomIn/><b>Digital Zoom</b>: {ratioToFloat(metadata.digitalZoomRatio)}</p>
-            )}
-            {metadata.exposure !== undefined && (
-                <p><Exposure/><b>Exposure</b>: {ratioToFloat(metadata.exposure)}</p>
-            )}
-            {metadata.exposureMode !== undefined && (
-                <p><Exposure/><b>Exposure Mode</b>: {ExposureMode[metadata.exposureMode]}</p>
-            )}
-            {metadata.exposureProgram !== undefined && (
-                <p><Exposure/><b>Exposure Program</b>: {ExposureProgram[metadata.exposureProgram]}</p>
-            )}
-            {metadata.shutterSpeed !== undefined && (
-                <p><CameraTimer/><b>Shutter Speed</b>: {ratioToTime(metadata.shutterSpeed)}</p>
-            )}
-            {metadata.aperture !== undefined && (
-                <p><Camera/><b>Aperture</b>: {ratioToFloat(metadata.aperture)}</p>
-            )}
-            {metadata.focalLength !== undefined && (
-                <p><AngleAcute/><b>Focal Length</b>: {ratioToFloat(metadata.focalLength)}mm</p>
-            )}
-            {metadata.focalLength35mm !== undefined && (
-                <p><AngleAcute/><b>Focal Length (35mm equivalent)</b>: {ratioToFloat(metadata.focalLength35mm)}mm</p>
-            )}
-            {metadata.isoSpeedRating !== undefined && (
-                <p><b>ISO</b>: {metadata.isoSpeedRating}</p>
-            )}
-            {metadata.flash !== undefined && (
-                <Fragment>
-                    <p><Flash/><b>Flash</b></p>
-                    <p><b>Available</b>: {metadata.flash.available ? "Yes" : "No"}</p>
-                    <p><b>Fired</b>: {metadata.flash.fired ? "Yes" : "No"}</p>
-                    <p><b>Red Eye Reduction</b>: {metadata.flash.redEyeReduction ? "Yes" : "No"}</p>
-                    <p><b>Strobe Detection Available</b>: {metadata.flash.strobeDetection.available ? "Yes" : "No"}</p>
-                    <p><b>Strobe Detection Used</b>: {metadata.flash.strobeDetection.detected ? "Yes" : "No"}</p>
-                    {metadata.flash.mode !== undefined && (
-                        <p><b>Flash Mode</b>: {FlashMode[metadata.flash.mode]}</p>
-                    )}
-                </Fragment>
-            )}
-            {metadata.lightSource !== undefined && (
-                <p><WhiteBalanceIncandescent/><b>Light source</b>: {LightSource[metadata.lightSource]}</p>
-            )}
-            {metadata.meteringMode !== undefined && (
-                <p><b>Metering mode</b>: {MeteringMode[metadata.meteringMode]}</p>
-            )}
-            {metadata.whiteBalance !== undefined && (
-                <p><WhiteBalanceIncandescent/><b>White balance</b>: {WhiteBalance[metadata.whiteBalance]}</p>
-            )}
-            {metadata.sceneMode !== undefined && (
-                <p><b>Scene Mode</b>: {SceneMode[metadata.sceneMode]}</p>
-            )}
-            {metadata.contrast !== undefined && (
-                <p><BrightnessMedium/><b>Contrast Processing</b>: {ContrastProcessing[metadata.contrast]}</p>
-            )}
-            {metadata.sharpness !== undefined && (
-                <p><Blur/><b>Sharpness Processing</b>: {SharpnessProcessing[metadata.sharpness]}</p>
-            )}
-            {metadata.subjectDistance !== undefined && (
-                <p><ArrowExpandHorizontal/><b>Subject Distance</b>: {metadata.subjectDistance}</p>
-            )}
-            {metadata.subjectDistanceRange !== undefined && (
-                <p><ArrowExpandHorizontal/><b>Subject Distance
-                    Range</b>: {SubjectDistanceRange[metadata.subjectDistanceRange]}</p>
-            )}
-            <img src={image.url + "t"} alt=""/>
-            <br/>
-            <input
-                type="submit"
-                value="Save"
+            <img src={image.url + "l"} alt=""/>
+            <List dense>
+                <ListItem dense>
+                    <ListItemIcon><Info/></ListItemIcon>
+                    <ListItemText primary="Id" secondary={image.id}/>
+                </ListItem>
+                <ListItem dense>
+                    <ListItemText inset primary="Owner" secondary={image.owner}/>
+                </ListItem>
+                <ListItem dense>
+                    <ListItemText inset primary="State" secondary={image.state}/>
+                </ListItem>
+                <ListItem dense>
+                    <ListItemIcon><Tag/></ListItemIcon>
+                    {/* TODO: Fix this ugly nesting */}
+                    <ListItemText
+                        primary="Title"
+                        secondary={<TextField
+                            fullWidth
+                            value={title}
+                            onChange={({target: {value}}) =>
+                                setTitle(value)}
+                        />}
+                    />
+                </ListItem>
+                <ListItem dense>
+                    {/* TODO: Fix this ugly nesting */}
+                    <ListItemText
+                        inset
+                        primary="Description"
+                        secondary={<TextField
+                            fullWidth
+                            value={description}
+                            onChange={({target: {value}}) =>
+                                setDescription(value)}
+                        />}
+                    />
+                </ListItem>
+                <ListItem dense>
+                    <ListItemIcon><File/></ListItemIcon>
+                    <ListItemText primary="Filename" secondary={image.original_name}/>
+                </ListItem>
+                <ListItem dense>
+                    <ListItemText inset primary="MIME Type" secondary={image.mime_type}/>
+                </ListItem>
+                <ListItem dense>
+                    <ListItemIcon><Event/></ListItemIcon>
+                    <ListItemText primary="Uploaded At" secondary={image.created_at}/>
+                </ListItem>
+                <ListItem dense>
+                    <ListItemText inset primary="Modified At" secondary={image.updated_at}/>
+                </ListItem>
+                <ImageMetadataView metadata={metadata}/>
+            </List>
+            <Button
                 onClick={() => update({
                     ...image,
                     title,
                     description,
                 })}
-            />
-            <input
-                type="submit"
-                value="Delete"
+            >Save</Button>
+            <Button
                 onClick={() => remove(image)}
-            />
+            >Delete</Button>
         </div>
     )
 }
-- 
GitLab