github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/server/camlistored/ui/image_detail.js (about)

     1  /*
     2  Copyright 2014 The Camlistore Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  goog.provide('cam.ImageDetail');
    18  
    19  goog.require('cam.BlobItemVideoContent');
    20  goog.require('cam.PropertySheetContainer');
    21  goog.require('cam.Thumber');
    22  
    23  // Renders the guts of the detail view for images.
    24  cam.ImageDetail = React.createClass({
    25  	displayName: 'ImageDetail',
    26  
    27  	IMG_MARGIN: 20,
    28  	PIGGY_WIDTH: 88,
    29  	PIGGY_HEIGHT: 62,
    30  
    31  	propTypes: {
    32  		backwardPiggy: React.PropTypes.bool.isRequired,
    33  		height: React.PropTypes.number.isRequired,
    34  		permanodeMeta: React.PropTypes.object,
    35  		resolvedMeta: React.PropTypes.object.isRequired,
    36  		width: React.PropTypes.number.isRequired,
    37  	},
    38  
    39  	isVideo_: function() {
    40  		return !this.isImage_();
    41  	},
    42  
    43  	isImage_: function() {
    44  		return Boolean(this.props.resolvedMeta.image);
    45  	},
    46  
    47  	componentWillReceiveProps: function(nextProps) {
    48  		if (this.props == nextProps || this.props.resolvedMeta.blobRef != nextProps.resolvedMeta.blobRef) {
    49  			this.thumber_ = nextProps.resolvedMeta.image && cam.Thumber.fromImageMeta(nextProps.resolvedMeta);
    50  			this.setState({imgHasLoaded: false});
    51  		}
    52  	},
    53  
    54  	componentWillMount: function() {
    55  		this.componentWillReceiveProps(this.props, true);
    56  	},
    57  
    58  	render: function() {
    59  		this.imgSize_ = this.getImgSize_();
    60  		return React.DOM.div({className:'detail-view', style: this.getStyle_()}, [
    61  			this.getImg_(),
    62  			this.getPiggy_(),
    63  			this.getSidebar_(),
    64  		]);
    65  	},
    66  
    67  	getSidebar_: function() {
    68  		return cam.PropertySheetContainer({className:'detail-view-sidebar', style:this.getSidebarStyle_()}, [
    69  			this.getGeneralProperties_(),
    70  			this.getFileishProperties_(),
    71  			this.getImageProperties_(),
    72  		]);
    73  	},
    74  
    75  	getGeneralProperties_: function() {
    76  		if (!this.props.permanodeMeta) {
    77  			return null;
    78  		}
    79  		return cam.PropertySheet({key:'general', title:'Generalities'}, [
    80  			React.DOM.h1({className:'detail-title'}, this.getSinglePermanodeAttr_('title') || '<no title>'),
    81  			React.DOM.p({className:'detail-description'}, this.getSinglePermanodeAttr_('description') || '<no description>'),
    82  		]);
    83  	},
    84  
    85  	getFileishProperties_: function() {
    86  		var isFile = this.props.resolvedMeta.camliType == 'file';
    87  		var isDir = this.props.resolvedMeta.camliType == 'directory';
    88  		if (!isFile && !isDir) {
    89  			return null;
    90  		}
    91  		return cam.PropertySheet({className:'detail-fileish-properties', key:'file', title: isFile ? 'File' : 'Directory'}, [
    92  			React.DOM.table({}, [
    93  				React.DOM.tr({}, [
    94  					React.DOM.td({}, 'filename'),
    95  					React.DOM.td({}, isFile ? this.props.resolvedMeta.file.fileName : this.props.resolvedMeta.dir.fileName),
    96  				]),
    97  				React.DOM.tr({}, [
    98  					React.DOM.td({}, 'size'),
    99  					React.DOM.td({}, this.props.resolvedMeta.file.size + ' bytes'),  // TODO(aa): Humanize units
   100  				]),
   101  			]),
   102  		]);
   103  	},
   104  
   105  	getImageProperties_: function() {
   106  		if (!this.props.resolvedMeta.image) {
   107  			return null;
   108  		}
   109  
   110  		return cam.PropertySheet({className:'detail-image-properties', key:'image', title: 'Image'}, [
   111  			React.DOM.table({}, [
   112  				React.DOM.tr({}, [
   113  					React.DOM.td({}, 'width'),
   114  					React.DOM.td({}, this.props.resolvedMeta.image.width),
   115  				]),
   116  				React.DOM.tr({}, [
   117  					React.DOM.td({}, 'height'),
   118  					React.DOM.td({}, this.props.resolvedMeta.image.height),
   119  				]),
   120  				// TODO(aa): encoding type, exif data, etc.
   121  			]),
   122  		]);
   123  	},
   124  
   125  	getSinglePermanodeAttr_: function(name) {
   126  		return cam.permanodeUtils.getSingleAttr(this.props.permanodeMeta.permanode, name);
   127  	},
   128  
   129  	onImgLoad_: function() {
   130  		this.setState({imgHasLoaded:true});
   131  	},
   132  
   133  	getImg_: function() {
   134  		var transition = React.addons.TransitionGroup({transitionName: 'detail-img'}, []);
   135  		if (this.imgSize_) {
   136  			var ctor = this.props.resolvedMeta.image ? React.DOM.img : React.DOM.video;
   137  			transition.props.children.push(
   138  				ctor({
   139  					className: React.addons.classSet({
   140  						'detail-view-img': true,
   141  						'detail-view-img-loaded': this.isImage_() ? this.state.imgHasLoaded : true,
   142  					}),
   143  					controls: true,
   144  					// We want each image to have its own node in the DOM so that during the crossfade, we don't see the image jump to the next image's size.
   145  					key: 'img' + this.props.resolvedMeta.blobRef,
   146  					onLoad: this.isImage_() ? this.onImgLoad_ : null,
   147  					src: this.isImage_() ? this.thumber_.getSrc(this.imgSize_.height) : './download/' + this.props.resolvedMeta.blobRef + '/' + this.props.resolvedMeta.file.fileName,
   148  					style: this.getCenteredProps_(this.imgSize_.width, this.imgSize_.height)
   149  				})
   150  			);
   151  		}
   152  		return transition;
   153  	},
   154  
   155  	getPiggy_: function() {
   156  		var transition = React.addons.TransitionGroup({transitionName: 'detail-piggy'}, []);
   157  		if (this.isImage_() && !this.state.imgHasLoaded) {
   158  			transition.props.children.push(
   159  				cam.SpritedAnimation({
   160  					src: 'glitch/npc_piggy__x1_walk_png_1354829432.png',
   161  					className: React.addons.classSet({
   162  						'detail-view-piggy': true,
   163  						'detail-view-piggy-backward': this.props.backwardPiggy
   164  					}),
   165  					spriteWidth: this.PIGGY_WIDTH,
   166  					spriteHeight: this.PIGGY_HEIGHT,
   167  					sheetWidth: 8,
   168  					sheetHeight: 3,
   169  					interval: 30,
   170  					style: this.getCenteredProps_(this.PIGGY_WIDTH, this.PIGGY_HEIGHT)
   171  				}));
   172  		}
   173  		return transition;
   174  	},
   175  
   176  	getCenteredProps_: function(w, h) {
   177  		var avail = new goog.math.Size(this.props.width - this.getSidebarWidth_(), this.props.height);
   178  		return {
   179  			top: (avail.height - h) / 2,
   180  			left: (avail.width - w) / 2,
   181  			width: w,
   182  			height: h
   183  		}
   184  	},
   185  
   186  	getImgSize_: function() {
   187  		if (this.isVideo_()) {
   188  			return new goog.math.Size(this.props.width, this.props.height);
   189  		}
   190  		var rawSize = new goog.math.Size(this.props.resolvedMeta.image.width, this.props.resolvedMeta.image.height);
   191  		var available = new goog.math.Size(
   192  			this.props.width - this.getSidebarWidth_() - this.IMG_MARGIN * 2,
   193  			this.props.height - this.IMG_MARGIN * 2);
   194  		if (rawSize.height <= available.height && rawSize.width <= available.width) {
   195  			return rawSize;
   196  		}
   197  		return rawSize.scaleToFit(available);
   198  	},
   199  
   200  	getStyle_: function() {
   201  		return {
   202  			width: this.props.width,
   203  			height: this.props.height
   204  		}
   205  	},
   206  
   207  	getSidebarStyle_: function() {
   208  		return {
   209  			width: this.getSidebarWidth_()
   210  		}
   211  	},
   212  
   213  	getSidebarWidth_: function() {
   214  		return Math.max(this.props.width * 0.2, 300);
   215  	},
   216  });
   217  
   218  cam.ImageDetail.getAspect = function(blobref, searchSession) {
   219  	var rm = searchSession.getResolvedMeta(blobref);
   220  	var pm = searchSession.getMeta(blobref);
   221  
   222  	if (pm.camliType != 'permanode') {
   223  		pm = null;
   224  	}
   225  
   226  	return rm && (rm.image || cam.BlobItemVideoContent.isVideo(rm)) ? new cam.ImageDetail.Aspect(rm, pm) : null;
   227  
   228  	// We don't handle camliContentImage like BlobItemImage.getHandler does because that only tells us what image to display in the search results. It doesn't actually make the permanode an image or anything.
   229  };
   230  
   231  cam.ImageDetail.Aspect = function(resolvedMeta, permanodeMeta) {
   232  	this.resolvedMeta_ = resolvedMeta;
   233  	this.permanodeMeta_ = permanodeMeta;
   234  };
   235  
   236  cam.ImageDetail.Aspect.prototype.getTitle = function() {
   237  	return this.resolvedMeta_.image ? 'Image' : 'Video';
   238  };
   239  
   240  // TODO(aa): Piggy should move into cam.Detail and use an onload handler to turn on/off.
   241  cam.ImageDetail.Aspect.prototype.createContent = function(size, backwardPiggy) {
   242  	return cam.ImageDetail({
   243  		backwardPiggy: backwardPiggy,
   244  		height: size.height,
   245  		permanodeMeta: this.permanodeMeta_,
   246  		resolvedMeta: this.resolvedMeta_,
   247  		width: size.width,
   248  	});
   249  };