From b177a97d27edfa6de78d50391dd1179d2b67239a Mon Sep 17 00:00:00 2001
From: Mickael KERJEAN
Date: Wed, 30 May 2018 21:19:03 +1000
Subject: [PATCH] feature (image): EXIF viewer on photos
---
client/assets/img/info.svg | 103 +++++++++++++++
client/components/alert.js | 49 ++++---
client/components/icon.js | 3 +
client/components/index.js | 2 +-
client/components/modal.scss | 4 +-
client/components/prompt.js | 12 +-
client/helpers/alert.js | 15 +++
client/helpers/index.js | 1 +
client/pages/viewerpage/imageviewer.js | 160 +++++++++++++++++++++--
client/pages/viewerpage/imageviewer.scss | 21 +++
client/router.js | 3 +-
package.json | 1 +
server/ctrl/files.js | 3 +-
13 files changed, 334 insertions(+), 43 deletions(-)
create mode 100644 client/assets/img/info.svg
create mode 100644 client/helpers/alert.js
create mode 100644 client/pages/viewerpage/imageviewer.scss
diff --git a/client/assets/img/info.svg b/client/assets/img/info.svg
new file mode 100644
index 00000000..3f932794
--- /dev/null
+++ b/client/assets/img/info.svg
@@ -0,0 +1,103 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/components/alert.js b/client/components/alert.js
index 1e3a4129..c3a1ed04 100644
--- a/client/components/alert.js
+++ b/client/components/alert.js
@@ -2,38 +2,53 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Input, Button, Modal, NgIf } from './';
+import { alert } from '../helpers/'
import "./alert.scss";
-export class Alert extends React.Component {
+export class AlertModal extends React.Component {
constructor(props){
super(props);
+ this.state = {
+ appear: false,
+ value: null,
+ fns: {}
+ };
+ this.onSubmit = this.onSubmit.bind(this);
+ }
+
+ componentDidMount(){
+ alert.subscribe((Component, okCallback) => {
+ this.setState({
+ appear: true,
+ value: Component,
+ fns: {ok: okCallback || function(){}}
+ });
+ });
}
onSubmit(e){
e && e.preventDefault && e.preventDefault();
- this.props.onConfirm && this.props.onConfirm();
+ this.state.fns.ok();
+ this.setState({appear: false});
}
+ onEscapeKeyPress(e){
+ if(e.keyCode === 27 && this.state.fns){ this.onSubmit(); }
+ }
+
+
render() {
return (
-
+
-
- {this.props.message}
-
-
+
+ {this.state.value}
+
+
+
+
);
}
}
-
-Alert.propTypes = {
- appear: PropTypes.bool.isRequired,
- message: PropTypes.string.isRequired,
- onConfirm: PropTypes.func
-};
diff --git a/client/components/icon.js b/client/components/icon.js
index ee8da3f7..f094f6dc 100644
--- a/client/components/icon.js
+++ b/client/components/icon.js
@@ -33,6 +33,7 @@ import img_grid from '../assets/img/grid.svg';
import img_list from '../assets/img/list.svg';
import img_sort from '../assets/img/sort.svg';
import img_check from '../assets/img/check.svg';
+import img_info from '../assets/img/info.svg';
export const Icon = (props) => {
if(props.name === null) return null;
@@ -97,6 +98,8 @@ export const Icon = (props) => {
img = img_sort;
}else if(props.name === 'check'){
img = img_check;
+ }else if(props.name === 'info'){
+ img = img_info;
}else{
throw('unknown icon');
}
diff --git a/client/components/index.js b/client/components/index.js
index 02fc182a..b4a210d4 100644
--- a/client/components/index.js
+++ b/client/components/index.js
@@ -13,8 +13,8 @@ export { Uploader } from './uploader';
export { Bundle } from './bundle';
export { Modal } from './modal';
export { ModalPrompt } from './prompt';
+export { AlertModal } from './alert';
export { Notification } from './notification';
-export { Alert } from './alert';
export { Audio } from './audio';
export { Video } from './video';
export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown';
diff --git a/client/components/modal.scss b/client/components/modal.scss
index 2a64c778..053e4f4e 100644
--- a/client/components/modal.scss
+++ b/client/components/modal.scss
@@ -10,8 +10,8 @@
> div{
background: white;
- width: 65%;
- max-width: 300px;
+ width: 80%;
+ max-width: 310px;
padding: 40px 20px 0 20px;
border-radius: 2px;
}
diff --git a/client/components/prompt.js b/client/components/prompt.js
index fe8aeadf..d7d27aac 100644
--- a/client/components/prompt.js
+++ b/client/components/prompt.js
@@ -53,12 +53,12 @@ export class ModalPrompt extends React.Component {
{this.state.text}
diff --git a/client/helpers/alert.js b/client/helpers/alert.js
new file mode 100644
index 00000000..9e234b9e
--- /dev/null
+++ b/client/helpers/alert.js
@@ -0,0 +1,15 @@
+const Alert = function (){
+ let fn = null;
+
+ return {
+ now: function(Component, okCallback){
+ if(!fn){ return window.setTimeout(() => this.now(Component, okCallback), 50); }
+ fn(Component, okCallback);
+ },
+ subscribe: function(_fn){
+ fn = _fn;
+ }
+ };
+};
+
+export const alert = new Alert();
diff --git a/client/helpers/index.js b/client/helpers/index.js
index 6d189ca2..e72d0050 100644
--- a/client/helpers/index.js
+++ b/client/helpers/index.js
@@ -9,6 +9,7 @@ export { memory } from './memory';
export { prepare } from './navigate';
export { invalidate, http_get, http_post, http_delete } from './ajax';
export { prompt } from './prompt';
+export { alert } from './alert';
export { notify } from './notify';
export { gid } from './random';
export { leftPad } from './common';
diff --git a/client/pages/viewerpage/imageviewer.js b/client/pages/viewerpage/imageviewer.js
index 34e3dd37..e5398646 100644
--- a/client/pages/viewerpage/imageviewer.js
+++ b/client/pages/viewerpage/imageviewer.js
@@ -1,19 +1,151 @@
import React from 'react';
+import EXIF from 'exif-js';
+import path from 'path';
import { MenuBar } from './menubar';
+import { Icon, NgIf } from '../../components/';
+import { alert } from '../../helpers/';
+import './imageviewer.scss';
-export const ImageViewer = (props) => {
- const image_url = (size) => {
- return props.data+"&meta=true&size="+parseInt((window.innerWidth - 40)*size);
- };
- return (
-
-
-
-
})
-
-
- );
+export class ImageViewer extends React.Component{
+ constructor(props){
+ super(props);
+ this.state = {
+ info: null
+ };
+ }
+
+ openInfo(){
+ alert.now();
+ }
+
+ render(){
+ const image_url = (size) => {
+ return this.props.data+"&meta=true&size="+parseInt((window.innerWidth - 40)*size);
+ };
+ const isJpeg = (filename) => {
+ const ext = path.extname(filename).toLowerCase().substring(1);
+ return ext === "jpg" || ext === "jpeg";
+ };
+ return (
+
+
+
+
+
+
+
+
})
+
+
+ );
+ }
+}
+
+
+class Metadata extends React.Component {
+ constructor(props){
+ super(props);
+ this.state = {
+ date: null,
+ location: null,
+ iso: null,
+ aperture: null,
+ shutter: null,
+ model: null,
+ maker: null
+ };
+ }
+
+ componentDidMount(){
+ let self = this;
+ EXIF.getData(this.props.el, function(){
+ const metadata = EXIF.getAllTags(this);
+ window.metadata = metadata;
+ self.setState({
+ date: metadata['DateTime'] || metadata['DateTimeDigitized'] || metadata['DateTimeOriginal'] || metadata['GPSDateStamp'],
+ location: metadata['GPSLatitude'] && metadata['GPSLongitude'] && [
+ [ metadata['GPSLatitude'][0], metadata['GPSLatitude'][1], metadata['GPSLatitude'][2], metadata["GPSLatitudeRef"]],
+ [ metadata['GPSLongitude'][0], metadata['GPSLongitude'][1], metadata['GPSLongitude'][2], metadata["GPSLongitudeRef"]]
+ ],
+ maker: metadata['Make'],
+ model: metadata['Model'],
+ focal: metadata['FocalLength'],
+ aperture: metadata['FNumber'],
+ shutter: metadata['ExposureTime'],
+ iso: metadata['ISOSpeedRatings']
+ });
+ });
+ }
+
+ render(){
+ const display_camera = (model, focal) => {
+ if(!model && !focal) return "-";
+ if(!focal) return model;
+ return model+" ("+parseInt(focal)+"mm)";
+ };
+ const display_settings = (aperture, shutter, iso) => {
+ if(!aperture || !shutter || !iso) return "-";
+ return "f/"+parseInt(aperture*10)/10+" "+speed(shutter)+" ISO"+iso;
+ function speed(n){
+ if(n > 60) return (parseInt(n) / 60).toString()+"m";
+ if(n >= 1) return parseInt(n).toString()+"s";
+ return "1/"+parseInt(nearestPow2(1/n)).toString()+"s";
+ }
+ function nearestPow2(n){
+ const refs = [1,2,3,4,5,6,8,10,13,15,20,25,30,40,45,50,60,80,90,100,125,160,180,200,250,320,350,400,500,640,750,800,1000,1250,1500,1600,2000,2500,3000,3200,4000,5000,6000,6400,8000,12000,16000,32000,50000];
+ for(let i=0, l=refs.length; i {
+ if(!_date) return "-";
+ let date = _date.substring(0,10).replace(/:/g, "/");
+ let time = _date.substring(11,16).replace(":", "h");
+ if(Intl && Intl.DateTimeFormat){
+ date = Intl.DateTimeFormat().format(new Date(date));
+ }
+
+ let text = date;
+ if(time) text += " "+time;
+ return text;
+ };
+ const display_location = (location) => {
+ if(!location || location.length !== 2) return '-';
+
+ let text = location[0][0]+"°"+location[0][1]+"'"+location[0][2]+"''"+location[0][3];
+ text += " ";
+ text += location[1][0]+"°"+location[1][1]+"'"+location[1][2]+"''"+location[1][3];
+ return text;
+ };
+
+
+ return (
+
+
+ Date:
+ {display_date(this.state.date)}
+
+
+
+ Settings:
+ {display_settings(this.state.aperture, this.state.shutter, this.state.iso)}
+
+
+ Camera:
+ {display_camera(this.state.model,this.state.focal)}
+
+
+ );
+ }
}
diff --git a/client/pages/viewerpage/imageviewer.scss b/client/pages/viewerpage/imageviewer.scss
new file mode 100644
index 00000000..3108f55f
--- /dev/null
+++ b/client/pages/viewerpage/imageviewer.scss
@@ -0,0 +1,21 @@
+.component_metadata{
+ .label{
+ display: inline-block;
+ width: 70px;
+ font-size: 0.95em;
+ text-align: right;
+ padding-right: 10px;
+ color: var(--color);
+ vertical-align: top;
+ }
+ .value{
+ display: inline-block;
+ width: calc(100% - 80px);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: top;
+ letter-spacing: -0.5px;
+ &.small{font-size: 0.95em; letter-spacing: -1px;}
+ }
+}
diff --git a/client/router.js b/client/router.js
index 26ac0c01..2da1433d 100644
--- a/client/router.js
+++ b/client/router.js
@@ -2,7 +2,7 @@ import React from 'react';
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
import { NotFoundPage, ConnectPage, HomePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
import { Bundle, URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
-import { ModalPrompt, Notification, Audio, Video } from './components/';
+import { ModalPrompt, AlertModal, Notification, Audio, Video } from './components/';
export default class AppRouter extends React.Component {
render() {
@@ -19,6 +19,7 @@ export default class AppRouter extends React.Component {
+
);
diff --git a/package.json b/package.json
index 6574ce9e..908ace8f 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"css-loader": "^0.28.10",
"dropbox": "^2.5.3",
"ejs": "^2.5.6",
+ "exif-js": "^2.3.0",
"html-loader": "^0.4.5",
"html-webpack-plugin": "^2.28.0",
"http-server": "^0.9.0",
diff --git a/server/ctrl/files.js b/server/ctrl/files.js
index ac94b881..f8c06ac6 100644
--- a/server/ctrl/files.js
+++ b/server/ctrl/files.js
@@ -59,7 +59,7 @@ app.get('/cat', function(req, res){
let endpoint = config.transcoder.url;
if(req.query.size){
- endpoint += "?size="+req.query.size;
+ endpoint += "?size="+req.query.size+"&meta="+(req.query.meta === "true" ? "true": "false");
}
const post_request = request({
method: "POST",
@@ -68,7 +68,6 @@ app.get('/cat', function(req, res){
});
return form.pipe(post_request)
.on('error', (err) => {
- console.log(err);
res.status(500).end();
})
.pipe(res);