mirror of https://github.com/perkeep/perkeep.git
275 lines
7.9 KiB
JavaScript
275 lines
7.9 KiB
JavaScript
/*
|
|
Copyright 2013 Google Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
goog.provide('cam.DetailView');
|
|
|
|
goog.require('goog.array');
|
|
goog.require('goog.events.EventHandler');
|
|
goog.require('goog.math.Size');
|
|
goog.require('goog.object');
|
|
goog.require('goog.string');
|
|
|
|
goog.require('cam.AnimationLoop');
|
|
goog.require('cam.imageUtil');
|
|
goog.require('cam.Navigator');
|
|
goog.require('cam.SearchSession');
|
|
goog.require('cam.SpritedAnimation');
|
|
|
|
cam.DetailView = React.createClass({
|
|
displayName: 'DetailView',
|
|
|
|
IMG_MARGIN: 20,
|
|
PIGGY_WIDTH: 88,
|
|
PIGGY_HEIGHT: 62,
|
|
|
|
propTypes: {
|
|
blobref: React.PropTypes.string.isRequired,
|
|
searchSession: React.PropTypes.instanceOf(cam.SearchSession).isRequired,
|
|
searchURL: React.PropTypes.instanceOf(goog.Uri).isRequired,
|
|
oldURL: React.PropTypes.instanceOf(goog.Uri).isRequired,
|
|
getDetailURL: React.PropTypes.func.isRequired,
|
|
navigator: React.PropTypes.instanceOf(cam.Navigator).isRequired,
|
|
keyEventTarget: React.PropTypes.object.isRequired, // An event target we will addEventListener() on to receive key events.
|
|
width: React.PropTypes.number.isRequired,
|
|
height: React.PropTypes.number.isRequired,
|
|
},
|
|
|
|
getInitialState: function() {
|
|
this.imgSize_ = null;
|
|
this.lastImageHeight_ = 0;
|
|
this.pendingNavigation_ = 0;
|
|
this.eh_ = new goog.events.EventHandler(this);
|
|
|
|
return {
|
|
imgHasLoaded: false,
|
|
backwardPiggy: false,
|
|
};
|
|
},
|
|
|
|
componentWillReceiveProps: function(nextProps) {
|
|
if (this.props.blobref != nextProps.blobref) {
|
|
this.imgSize_ = null;
|
|
this.lastImageHeight_ = 0;
|
|
this.setState({imgHasLoaded: false});
|
|
}
|
|
},
|
|
|
|
componentDidMount: function(root) {
|
|
this.eh_.listen(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.searchUpdated_);
|
|
this.eh_.listen(this.props.keyEventTarget, 'keyup', this.handleKeyUp_);
|
|
this.searchUpdated_();
|
|
},
|
|
|
|
componentDidUpdate: function(prevProps, prevState) {
|
|
var img = this.getImageRef_();
|
|
if (img) {
|
|
// This function gets called multiple times, but the DOM de-dupes listeners for us. Thanks DOM.
|
|
img.getDOMNode().addEventListener('load', this.onImgLoad_);
|
|
}
|
|
},
|
|
|
|
render: function() {
|
|
this.imgSize_ = this.getImgSize_();
|
|
return (
|
|
React.DOM.div({className:'detail-view', style: this.getStyle_()},
|
|
this.getImg_(),
|
|
this.getPiggy_(),
|
|
React.DOM.div({className:'detail-view-sidebar', key:'sidebar', style: this.getSidebarStyle_()},
|
|
React.DOM.a({key:'search-link', href:this.props.searchURL.toString()}, 'Back to search'),
|
|
' - ',
|
|
React.DOM.a({key:'old-link', href:this.props.oldURL.toString()}, 'Old and busted'),
|
|
React.DOM.pre({key:'sidebar-pre'}, JSON.stringify(this.getPermanodeMeta_(), null, 2)))));
|
|
},
|
|
|
|
componentWillUnmount: function() {
|
|
this.eh_.dispose();
|
|
},
|
|
|
|
handleKeyUp_: function(e) {
|
|
if (e.keyCode == goog.events.KeyCodes.LEFT) {
|
|
this.navigate_(-1);
|
|
} else if (e.keyCode == goog.events.KeyCodes.RIGHT) {
|
|
this.navigate_(1);
|
|
} else if (e.keyCode == goog.events.KeyCodes.ESC) {
|
|
this.props.navigator.navigate(this.props.searchURL);
|
|
}
|
|
},
|
|
|
|
navigate_: function(offset) {
|
|
this.pendingNavigation_ = offset;
|
|
this.setState({backwardPiggy: offset < 0});
|
|
this.handlePendingNavigation_();
|
|
},
|
|
|
|
handlePendingNavigation_: function() {
|
|
if (!this.pendingNavigation_) {
|
|
return;
|
|
}
|
|
|
|
var results = this.props.searchSession.getCurrentResults();
|
|
var index = goog.array.findIndex(results.blobs, function(elm) {
|
|
return elm.blob == this.props.blobref;
|
|
}.bind(this));
|
|
|
|
if (index == -1) {
|
|
this.props.searchSession.loadMoreResults();
|
|
return;
|
|
}
|
|
|
|
index += this.pendingNavigation_;
|
|
if (index < 0) {
|
|
this.pendingNavigation_ = 0;
|
|
console.log('Cannot navigate past beginning of search result.');
|
|
return;
|
|
}
|
|
|
|
if (index >= results.blobs.length) {
|
|
if (this.props.searchSession.isComplete()) {
|
|
this.pendingNavigation_ = 0;
|
|
console.log('Cannot navigate past end of search result.');
|
|
} else {
|
|
this.props.searchSession.loadMoreResults();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.props.navigator.navigate(this.props.getDetailURL(results.blobs[index].blob));
|
|
},
|
|
|
|
onImgLoad_: function() {
|
|
this.setState({imgHasLoaded:true});
|
|
},
|
|
|
|
searchUpdated_: function() {
|
|
this.handlePendingNavigation_();
|
|
|
|
if (this.getPermanodeMeta_()) {
|
|
this.forceUpdate();
|
|
return;
|
|
}
|
|
|
|
if (this.props.searchSession.isComplete()) {
|
|
// TODO(aa): 404 UI.
|
|
var error = goog.string.subs('Could not find blobref %s in search session.', this.props.blobref);
|
|
alert(error);
|
|
throw new Error(error);
|
|
}
|
|
|
|
// TODO(aa): This can be inefficient in the case of a fresh page load if we have to load lots of pages to find the blobref.
|
|
// Our search protocol needs to be updated to handle the case of paging ahead to a particular item.
|
|
this.props.searchSession.loadMoreResults();
|
|
},
|
|
|
|
getImg_: function() {
|
|
var transition = React.addons.TransitionGroup({transitionName: 'detail-img'}, []);
|
|
if (this.imgSize_) {
|
|
transition.props.children.push(
|
|
React.DOM.img({
|
|
className: React.addons.classSet({
|
|
'detail-view-img': true,
|
|
'detail-view-img-loaded': this.state.imgHasLoaded
|
|
}),
|
|
// 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.
|
|
key: this.getImageId_(),
|
|
ref: this.getImageId_(),
|
|
src: this.getSrc_(),
|
|
style: this.getCenteredProps_(this.imgSize_.width, this.imgSize_.height)
|
|
})
|
|
);
|
|
}
|
|
return transition;
|
|
},
|
|
|
|
getPiggy_: function() {
|
|
var transition = React.addons.TransitionGroup({transitionName: 'detail-piggy'}, []);
|
|
if (!this.state.imgHasLoaded) {
|
|
transition.props.children.push(
|
|
cam.SpritedAnimation({
|
|
src: 'glitch/npc_piggy__x1_walk_png_1354829432.png',
|
|
className: React.addons.classSet({
|
|
'detail-view-piggy': true,
|
|
'detail-view-piggy-backward': this.state.backwardPiggy
|
|
}),
|
|
spriteWidth: this.PIGGY_WIDTH,
|
|
spriteHeight: this.PIGGY_HEIGHT,
|
|
sheetWidth: 8,
|
|
sheetHeight: 3,
|
|
interval: 30,
|
|
style: this.getCenteredProps_(this.PIGGY_WIDTH, this.PIGGY_HEIGHT)
|
|
}));
|
|
}
|
|
return transition;
|
|
},
|
|
|
|
getCenteredProps_: function(w, h) {
|
|
var avail = new goog.math.Size(this.props.width - this.getSidebarWidth_(), this.props.height);
|
|
return {
|
|
top: (avail.height - h) / 2,
|
|
left: (avail.width - w) / 2,
|
|
width: w,
|
|
height: h
|
|
}
|
|
},
|
|
|
|
getSrc_: function() {
|
|
this.lastImageHeight_ = cam.imageUtil.getSizeToRequest(this.imgSize_.height, this.lastImageHeight_);
|
|
var uri = new goog.Uri(this.getPermanodeMeta_().thumbnailSrc);
|
|
uri.setParameterValue('mh', this.lastImageHeight_);
|
|
return uri.toString();
|
|
},
|
|
|
|
getImgSize_: function() {
|
|
var meta = this.getPermanodeMeta_();
|
|
if (!meta) {
|
|
return null;
|
|
}
|
|
var aspect = new goog.math.Size(meta.thumbnailWidth, meta.thumbnailHeight);
|
|
var available = new goog.math.Size(
|
|
this.props.width - this.getSidebarWidth_() - this.IMG_MARGIN * 2,
|
|
this.props.height - this.IMG_MARGIN * 2);
|
|
return aspect.scaleToFit(available);
|
|
},
|
|
|
|
getStyle_: function() {
|
|
return {
|
|
width: this.props.width,
|
|
height: this.props.height
|
|
}
|
|
},
|
|
|
|
getSidebarStyle_: function() {
|
|
return {
|
|
width: this.getSidebarWidth_()
|
|
}
|
|
},
|
|
|
|
getSidebarWidth_: function() {
|
|
return Math.max(this.props.width * 0.2, 300);
|
|
},
|
|
|
|
getPermanodeMeta_: function() {
|
|
return this.props.searchSession.getCurrentResults().description.meta[this.props.blobref];
|
|
},
|
|
|
|
getImageRef_: function() {
|
|
return this.refs[this.getImageId_()];
|
|
},
|
|
|
|
getImageId_: function() {
|
|
return 'img' + this.props.blobref;
|
|
}
|
|
});
|