mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-11-04 13:35:46 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			446 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			446 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import React from "react";
 | 
						|
import EXIF from "exif-js";
 | 
						|
import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 | 
						|
import { withRouter } from "react-router-dom";
 | 
						|
 | 
						|
import { NgIf, Icon, EventReceiver, MapShot, Button } from "../../components/";
 | 
						|
import { t } from "../../locales/";
 | 
						|
import "./image_exif.scss";
 | 
						|
 | 
						|
class Exif extends React.Component {
 | 
						|
    constructor(props) {
 | 
						|
        super(props);
 | 
						|
        if (new.target === Exif) {
 | 
						|
            throw new TypeError("Cannot construct Popup instances directly");
 | 
						|
        }
 | 
						|
        this.state = {
 | 
						|
            date: null,
 | 
						|
            location: null,
 | 
						|
            iso: null,
 | 
						|
            aperture: null,
 | 
						|
            shutter: null,
 | 
						|
            model: null,
 | 
						|
            maker: null,
 | 
						|
            all: null,
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    formatDate(def = null) {
 | 
						|
        if (!this.state.date) return def;
 | 
						|
        return this.state.date.toLocaleDateString(
 | 
						|
            navigator.language,
 | 
						|
            { day: "numeric", year: "numeric", month: "short", day: "numeric" },
 | 
						|
        );
 | 
						|
    }
 | 
						|
    formatTime() {
 | 
						|
        if (!this.state.date) return null;
 | 
						|
        return this.state.date.toLocaleTimeString(
 | 
						|
            "en-us",
 | 
						|
            { weekday: "short", hour: "2-digit", minute: "2-digit" },
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    locationMap() {
 | 
						|
        const display_location = (location) => {
 | 
						|
            if (!location || location.length !== 2) return null;
 | 
						|
            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;
 | 
						|
        };
 | 
						|
        let url = "https://www.google.com/maps/search/?api=1&query=";
 | 
						|
        url += display_location(this.state.location);
 | 
						|
        return url;
 | 
						|
    }
 | 
						|
 | 
						|
    format(key, def="") {
 | 
						|
        if (!this.state[key]) return def;
 | 
						|
        if (key === "focal") {
 | 
						|
            return this.state.focal+"mm";
 | 
						|
        } else if (key === "shutter") {
 | 
						|
            if (this.state.shutter > 60) return this.state.shutter+"m";
 | 
						|
            else if (this.state.shutter > 1) return this.state.shutter+"s";
 | 
						|
            return "1/"+parseInt(this.state.shutter.denominator / this.state.shutter.numerator)+"s";
 | 
						|
        } else if (key === "iso") {
 | 
						|
            return "ISO"+this.state.iso;
 | 
						|
        } else if (key === "aperture") {
 | 
						|
            return "ƒ"+parseInt(this.state.aperture*10)/10;
 | 
						|
        } else if (key === "dimension") {
 | 
						|
            if (this.state.dimension.length !== 2 || !this.state.dimension[0] ||
 | 
						|
                !this.state.dimension[1]) return "-";
 | 
						|
            return this.state.dimension[0]+"x"+this.state.dimension[1];
 | 
						|
        }
 | 
						|
        return this.state[key];
 | 
						|
    }
 | 
						|
 | 
						|
    refresh() {
 | 
						|
        const self = this;
 | 
						|
        const $photo = document.querySelector("img.photo");
 | 
						|
        if (!$photo) return;
 | 
						|
 | 
						|
        EXIF.getData($photo, function() {
 | 
						|
            const metadata = EXIF.getAllTags(this);
 | 
						|
            self.setState({
 | 
						|
                date: to_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"],
 | 
						|
                    ],
 | 
						|
                ] || null,
 | 
						|
                maker: metadata["Make"] || null,
 | 
						|
                model: metadata["Model"] || null,
 | 
						|
                focal: metadata["FocalLength"] || null,
 | 
						|
                aperture: metadata["FNumber"] || null,
 | 
						|
                shutter: metadata["ExposureTime"] || null,
 | 
						|
                iso: metadata["ISOSpeedRatings"] || null,
 | 
						|
                dimension: metadata["PixelXDimension"] && metadata["PixelYDimension"] && [
 | 
						|
                    metadata["PixelXDimension"],
 | 
						|
                    metadata["PixelYDimension"],
 | 
						|
                ] || null,
 | 
						|
                all: Object.keys(metadata).length === 0 ? null : metadata,
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        function to_date(str) {
 | 
						|
            if (!str) return null;
 | 
						|
            return new Date(...str.split(/[ :]/));
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    clear() {
 | 
						|
        const new_state = Object.assign({}, this.state);
 | 
						|
        Object.keys(new_state).map((key) => new_state[key] = null);
 | 
						|
        this.setState(new_state);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class SmallExif extends Exif {
 | 
						|
    constructor(props) {
 | 
						|
        super(props);
 | 
						|
    }
 | 
						|
 | 
						|
    componentDidMount() {
 | 
						|
        this.refresh();
 | 
						|
    }
 | 
						|
 | 
						|
    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 "ƒ/"+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<l; i++) {
 | 
						|
                    if (refs[i] <= n) continue;
 | 
						|
                    return refs[i] - n < refs[i-1] - n ? refs[i] : refs[i-1];
 | 
						|
                }
 | 
						|
                return n;
 | 
						|
            }
 | 
						|
        };
 | 
						|
        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 (
 | 
						|
            <div className="component_metadata">
 | 
						|
                <div>
 | 
						|
                    <span className="label no-select">{ t("Date") }: </span>
 | 
						|
                    <span className="value">{this.formatDate("-")}</span>
 | 
						|
                </div>
 | 
						|
                <div>
 | 
						|
                    <span className="label no-select">{ t("Location") }: </span>
 | 
						|
                    <span className="value small">
 | 
						|
                        <a href={"https://www.google.com/maps/search/?api=1&query="+display_location(this.state.location)}>
 | 
						|
                            {display_location(this.state.location)}
 | 
						|
                        </a>
 | 
						|
                    </span>
 | 
						|
                </div>
 | 
						|
                <div>
 | 
						|
                    <span className="label no-select">{ t("Settings") }: </span>
 | 
						|
                    <span className="value">
 | 
						|
                        {display_settings(this.state.aperture, this.state.shutter, this.state.iso)}
 | 
						|
                    </span>
 | 
						|
                </div>
 | 
						|
                <div>
 | 
						|
                    <span className="label no-select">{ t("Camera") }: </span>
 | 
						|
                    <span className="value">
 | 
						|
                        {display_camera(this.state.model, this.state.focal)}
 | 
						|
                    </span>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
class LargeExifClass extends Exif {
 | 
						|
    constructor(props) {
 | 
						|
        super(props);
 | 
						|
        this.state["show_more"] = false;
 | 
						|
        this.state["_"] = null;
 | 
						|
    }
 | 
						|
 | 
						|
    componentDidMount() {
 | 
						|
        this.refresh_handler(this.props);
 | 
						|
    }
 | 
						|
    UNSAFE_componentWillReceiveProps(props) {
 | 
						|
        this.refresh_handler(props);
 | 
						|
    }
 | 
						|
 | 
						|
    refresh_handler(props) {
 | 
						|
        if (props.ready === true && props.show === true && this.state["_"] !== props.data) {
 | 
						|
            this.setState({ "_": props.data });
 | 
						|
            this.refresh();
 | 
						|
        } else if (props.ready === false && props.show === true && this.state["_"] !== null) {
 | 
						|
            this.setState({ "_": null });
 | 
						|
            this.clear();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    all_meta() {
 | 
						|
        if (!this.state.all) return null;
 | 
						|
        const formatKey = (str) => {
 | 
						|
            return str.replace(/([A-Z][a-z])/g, " $1");
 | 
						|
        };
 | 
						|
        const formatValue = (str) => {
 | 
						|
            if (!this.state.all || this.state.all[str] === undefined) return "-";
 | 
						|
            if (typeof this.state.all[str] === "number") {
 | 
						|
                return parseInt(this.state.all[str]*100)/100;
 | 
						|
            } else if (this.state.all[str].denominator !== undefined &&
 | 
						|
                       this.state.all[str].numerator !== undefined) {
 | 
						|
                if (this.state.all[str].denominator === 1) {
 | 
						|
                    return this.state.all[str].numerator;
 | 
						|
                } else if (this.state.all[str].numerator > this.state.all[str].denominator) {
 | 
						|
                    return parseInt(
 | 
						|
                        this.state.all[str].numerator * 10 / this.state.all[str].denominator,
 | 
						|
                    ) / 10;
 | 
						|
                } else {
 | 
						|
                    return this.state.all[str].numerator+"/"+this.state.all[str].denominator;
 | 
						|
                }
 | 
						|
            } else if (typeof this.state.all[str] === "string") {
 | 
						|
                return this.state.all[str];
 | 
						|
            } else if (Array.isArray(this.state.all[str])) {
 | 
						|
                let arr = this.state.all[str];
 | 
						|
                if (arr.length > 15) {
 | 
						|
                    arr = arr.slice(0, 3);
 | 
						|
                    arr.push("...");
 | 
						|
                }
 | 
						|
                return arr.toString().split(",").join(", ");
 | 
						|
            } else {
 | 
						|
                return JSON.stringify(this.state.all[str], null, 2);
 | 
						|
            }
 | 
						|
        };
 | 
						|
        const alphabetical = (list) => {
 | 
						|
            return list.sort((a, b) => {
 | 
						|
                if (a.toLowerCase().trim() < b.toLowerCase().trim()) return -1;
 | 
						|
                else if (a.toLowerCase().trim() > b.toLowerCase().trim()) return +1;
 | 
						|
                return 0;
 | 
						|
            });
 | 
						|
        };
 | 
						|
 | 
						|
        return (
 | 
						|
            <div>
 | 
						|
                {
 | 
						|
                    alphabetical(Object.keys(this.state.all)).map((key, i) => {
 | 
						|
                        if (key === "undefined") return null;
 | 
						|
                        else if (key === "thumbnail") return null;
 | 
						|
                        return (
 | 
						|
                            <div key={i} className="meta_key">
 | 
						|
                                <div className="title">{formatKey(key)}: </div>
 | 
						|
                                <div className="value">{formatValue(key)}</div>
 | 
						|
                            </div>
 | 
						|
                        );
 | 
						|
                    })
 | 
						|
                }
 | 
						|
            </div>
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    render() {
 | 
						|
        const DMSToDD = (d) => {
 | 
						|
            if (!d || d.length !== 4) return null;
 | 
						|
            const [degrees, minutes, seconds, direction] = d;
 | 
						|
            const dd = degrees + minutes/60 + seconds/(60*60);
 | 
						|
            return direction == "S" || direction == "W" ? -dd : dd;
 | 
						|
        };
 | 
						|
 | 
						|
        const formatCameraHeadline = () => {
 | 
						|
            if (!this.format("model") || !this.format("focal")) {
 | 
						|
                return (
 | 
						|
                    <ReactCSSTransitionGroup
 | 
						|
                        transitionName="placeholder" transitionLeave={false} transitionEnter={true}
 | 
						|
                        transitionAppear={true} transitionEnterTimeout={500}
 | 
						|
                        transitionAppearTimeout={500}>
 | 
						|
                        <span key={this.format("model")+this.format("model")}>-</span>
 | 
						|
                    </ReactCSSTransitionGroup>
 | 
						|
                );
 | 
						|
            }
 | 
						|
            return (
 | 
						|
                <ReactCSSTransitionGroup
 | 
						|
                    transitionName="text" transitionLeave={false} transitionEnter={true}
 | 
						|
                    transitionAppear={true} transitionEnterTimeout={300}
 | 
						|
                    transitionAppearTimeout={300}>
 | 
						|
                    <span key={this.format("model")}>
 | 
						|
                        {this.format("model")} ({this.format("focal")})
 | 
						|
                    </span>
 | 
						|
                </ReactCSSTransitionGroup>
 | 
						|
            );
 | 
						|
        };
 | 
						|
        const formatCameraDescription = () => {
 | 
						|
            if (!this.format("shutter") || !this.format("aperture") || !this.format("focal")) {
 | 
						|
                return (
 | 
						|
                    <ReactCSSTransitionGroup
 | 
						|
                        transitionName="placeholder" transitionLeave={false} transitionEnter={true}
 | 
						|
                        transitionAppear={true} transitionEnterTimeout={500}
 | 
						|
                        transitionAppearTimeout={500}>
 | 
						|
                        <span key={this.format("shutter")+this.format("aperture")+this.format("focal")}>-</span>
 | 
						|
                    </ReactCSSTransitionGroup>
 | 
						|
                );
 | 
						|
            }
 | 
						|
            return (
 | 
						|
                <ReactCSSTransitionGroup
 | 
						|
                    transitionName="text" transitionLeave={false} transitionEnter={true}
 | 
						|
                    transitionAppear={true} transitionEnterTimeout={300}
 | 
						|
                    transitionAppearTimeout={300}>
 | 
						|
                    <span key={this.format("shutter")}>
 | 
						|
                        {this.format("aperture")} {this.format("shutter")} {this.format("iso")}
 | 
						|
                    </span>
 | 
						|
                </ReactCSSTransitionGroup>
 | 
						|
            );
 | 
						|
        };
 | 
						|
 | 
						|
        const formatCalendarHeadline = () => {
 | 
						|
            if (!this.formatDate()) {
 | 
						|
                return (
 | 
						|
                    <ReactCSSTransitionGroup
 | 
						|
                        transitionName="placeholder" transitionLeave={false} transitionEnter={true}
 | 
						|
                        transitionAppear={true} transitionEnterTimeout={500}
 | 
						|
                        transitionAppearTimeout={500}>
 | 
						|
                        <span key={this.formatDate()}>-</span>
 | 
						|
                    </ReactCSSTransitionGroup>
 | 
						|
                );
 | 
						|
            }
 | 
						|
            return (
 | 
						|
                <ReactCSSTransitionGroup
 | 
						|
                    transitionName="text" transitionLeave={false} transitionEnter={true}
 | 
						|
                    transitionAppear={true} transitionEnterTimeout={300}
 | 
						|
                    transitionAppearTimeout={300}>
 | 
						|
                    <span key={this.formatDate()}>
 | 
						|
                        {this.formatDate()}
 | 
						|
                    </span>
 | 
						|
                </ReactCSSTransitionGroup>
 | 
						|
            );
 | 
						|
        };
 | 
						|
 | 
						|
        const formatCalendarDescription = () => {
 | 
						|
            if (!this.formatTime()) {
 | 
						|
                return (
 | 
						|
                    <ReactCSSTransitionGroup
 | 
						|
                        transitionName="placeholder" transitionLeave={false} transitionEnter={true}
 | 
						|
                        transitionAppear={true} transitionEnterTimeout={500}
 | 
						|
                        transitionAppearTimeout={500}>
 | 
						|
                        <span key={this.formatTime()}>-</span>
 | 
						|
                    </ReactCSSTransitionGroup>
 | 
						|
                );
 | 
						|
            }
 | 
						|
            return (
 | 
						|
                <ReactCSSTransitionGroup
 | 
						|
                    transitionName="text" transitionLeave={false} transitionEnter={true}
 | 
						|
                    transitionAppear={true} transitionEnterTimeout={300}
 | 
						|
                    transitionAppearTimeout={300}>
 | 
						|
                    <span key={this.formatTime()}>
 | 
						|
                        {this.formatTime()}
 | 
						|
                    </span>
 | 
						|
                </ReactCSSTransitionGroup>
 | 
						|
            );
 | 
						|
        };
 | 
						|
 | 
						|
        return (
 | 
						|
            <div>
 | 
						|
                <div className="content_box">
 | 
						|
                    <Icon name="schedule" />
 | 
						|
                    <div className="headline">{formatCalendarHeadline()}</div>
 | 
						|
                    <div className="description">{formatCalendarDescription()}</div>
 | 
						|
                </div>
 | 
						|
 | 
						|
                <div className="content_box">
 | 
						|
                    <Icon name="camera" />
 | 
						|
                    <div className="headline">
 | 
						|
                        { formatCameraHeadline() }
 | 
						|
                    </div>
 | 
						|
                    <div className="description">
 | 
						|
                        { formatCameraDescription() }
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
 | 
						|
 | 
						|
                <NgIf cond={this.state.location !== null}>
 | 
						|
                    <ReactCSSTransitionGroup
 | 
						|
                        transitionName="image" transitionLeave={false} transitionEnter={true}
 | 
						|
                        transitionAppear={true} transitionEnterTimeout={300}
 | 
						|
                        transitionAppearTimeout={300}>
 | 
						|
                        <div key={JSON.stringify(this.state.location)}>
 | 
						|
                            <MapShot
 | 
						|
                                lat={DMSToDD(this.state.location && this.state.location[0])}
 | 
						|
                                lng={DMSToDD(this.state.location && this.state.location[1])} />
 | 
						|
                        </div>
 | 
						|
                    </ReactCSSTransitionGroup>
 | 
						|
                </NgIf>
 | 
						|
 | 
						|
                <NgIf cond={!!this.state.all} className="more">
 | 
						|
                    <ReactCSSTransitionGroup
 | 
						|
                        transitionName="image" transitionLeave={false} transitionEnter={true}
 | 
						|
                        transitionAppear={true} transitionEnterTimeout={300}
 | 
						|
                        transitionAppearTimeout={300}>
 | 
						|
                        <div key={this.state.all === null}>
 | 
						|
                            <Button
 | 
						|
                                onClick={() => this.setState({ show_more: !this.state.show_more })}
 | 
						|
                                theme="primary">
 | 
						|
                                MORE
 | 
						|
                            </Button>
 | 
						|
                        </div>
 | 
						|
                    </ReactCSSTransitionGroup>
 | 
						|
                </NgIf>
 | 
						|
                <ReactCSSTransitionGroup
 | 
						|
                    transitionName="image" transitionLeave={false} transitionEnter={true}
 | 
						|
                    transitionAppear={true} transitionEnterTimeout={300}
 | 
						|
                    transitionAppearTimeout={300}>
 | 
						|
                    <NgIf
 | 
						|
                        className="more_container"
 | 
						|
                        key={this.state.show_more}
 | 
						|
                        cond={!!this.state.show_more}>
 | 
						|
                        { this.all_meta() }
 | 
						|
                    </NgIf>
 | 
						|
                </ReactCSSTransitionGroup>
 | 
						|
            </div>
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export const LargeExif = EventReceiver(withRouter(LargeExifClass));
 |