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 };