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 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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}

- this.setState({value: e.target.value})} /> -
{this.state.error} 
-
- - -
+ this.setState({value: e.target.value})} /> +
{this.state.error} 
+
+ + +
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)} +
+
+ Location: + {display_location(this.state.location)} +
+
+ 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);