mirror of https://github.com/perkeep/perkeep.git
Increase pigginess in new detail UI.
Change-Id: I4eb1acf7e6cd114edf7c5f66fcb4fb6ff8842bdb
This commit is contained in:
parent
e0773c28d1
commit
2d70e6794f
3
make.go
3
make.go
|
@ -345,7 +345,8 @@ func genEmbeds() error {
|
|||
uiEmbeds := buildSrcPath("server/camlistored/ui")
|
||||
serverEmbeds := buildSrcPath("pkg/server")
|
||||
reactEmbeds := buildSrcPath("third_party/react")
|
||||
for _, embeds := range []string{uiEmbeds, serverEmbeds, reactEmbeds} {
|
||||
glitchEmbeds := buildSrcPath("third_party/glitch")
|
||||
for _, embeds := range []string{uiEmbeds, serverEmbeds, reactEmbeds, glitchEmbeds} {
|
||||
args := []string{embeds}
|
||||
cmd := exec.Command(cmdName, args...)
|
||||
cmd.Env = append(cleanGoEnv(),
|
||||
|
|
|
@ -40,6 +40,7 @@ import (
|
|||
"camlistore.org/pkg/sorted"
|
||||
uistatic "camlistore.org/server/camlistored/ui"
|
||||
closurestatic "camlistore.org/server/camlistored/ui/closure"
|
||||
glitchstatic "camlistore.org/third_party/glitch"
|
||||
reactstatic "camlistore.org/third_party/react"
|
||||
)
|
||||
|
||||
|
@ -57,6 +58,7 @@ var (
|
|||
treePattern = regexp.MustCompile(`^tree/([^/]+)(/.*)?$`)
|
||||
closurePattern = regexp.MustCompile(`^closure/(([^/]+)(/.*)?)$`)
|
||||
reactPattern = regexp.MustCompile(`^react/(.+)$`)
|
||||
glitchPattern = regexp.MustCompile(`^glitch/(.+)$`)
|
||||
|
||||
disableThumbCache, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_THUMB_CACHE"))
|
||||
)
|
||||
|
@ -91,8 +93,9 @@ type UIHandler struct {
|
|||
|
||||
uiDir string // if sourceRoot != "", this is sourceRoot+"/server/camlistored/ui"
|
||||
|
||||
closureHandler http.Handler
|
||||
fileReactHandler http.Handler
|
||||
closureHandler http.Handler
|
||||
fileReactHandler http.Handler
|
||||
fileGlitchHandler http.Handler
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -213,10 +216,14 @@ func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, er
|
|||
}
|
||||
|
||||
if ui.sourceRoot != "" {
|
||||
ui.fileReactHandler, err = ui.makeEmbeddedReactHandler(ui.sourceRoot)
|
||||
ui.fileReactHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "react"), "react.js")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not make react handler: %s", err)
|
||||
}
|
||||
ui.fileGlitchHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "glitch"), "npc_piggy__x1_walk_png_1354829432.png")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not make glitch handler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
rootPrefix, _, err := ld.FindHandlerByType("root")
|
||||
|
@ -237,15 +244,6 @@ func (ui *UIHandler) makeClosureHandler(root string) (http.Handler, error) {
|
|||
return makeClosureHandler(root, "ui")
|
||||
}
|
||||
|
||||
func (ui *UIHandler) makeEmbeddedReactHandler(root string) (http.Handler, error) {
|
||||
path := filepath.Join("third_party", "react")
|
||||
h, err := makeFileServer(root, path, "react.js")
|
||||
if h != nil {
|
||||
log.Printf("Serving React from %s", path)
|
||||
}
|
||||
return h, err
|
||||
}
|
||||
|
||||
// makeClosureHandler returns a handler to serve Closure files.
|
||||
// root is either:
|
||||
// 1) empty: use the Closure files compiled in to the binary (if
|
||||
|
@ -277,11 +275,7 @@ func makeClosureHandler(root, handlerName string) (http.Handler, error) {
|
|||
}
|
||||
|
||||
path := filepath.Join("third_party", "closure", "lib", "closure")
|
||||
fs, err := makeFileServer(root, path, filepath.Join("goog", "base.js"))
|
||||
if fs != nil {
|
||||
log.Printf("%v: serving Closure from disk: %s", handlerName, filepath.Join(root, path))
|
||||
}
|
||||
return fs, err
|
||||
return makeFileServer(root, path, filepath.Join("goog", "base.js"))
|
||||
}
|
||||
|
||||
func makeFileServer(sourceRoot string, pathToServe string, expectedContentPath string) (http.Handler, error) {
|
||||
|
@ -344,18 +338,10 @@ func wantsDetailPage(req *http.Request) bool {
|
|||
return httputil.IsGet(req) && httputil.PathSuffix(req) == "detail.html"
|
||||
}
|
||||
|
||||
func wantsClosure(req *http.Request) bool {
|
||||
func getSuffixMatches(req *http.Request, pattern *regexp.Regexp) bool {
|
||||
if httputil.IsGet(req) {
|
||||
suffix := httputil.PathSuffix(req)
|
||||
return closurePattern.MatchString(suffix)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func wantsReact(req *http.Request) bool {
|
||||
if httputil.IsGet(req) {
|
||||
suffix := httputil.PathSuffix(req)
|
||||
return reactPattern.MatchString(suffix)
|
||||
return pattern.MatchString(suffix)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -375,10 +361,12 @@ func (ui *UIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||
ui.serveThumbnail(rw, req)
|
||||
case strings.HasPrefix(suffix, "tree/"):
|
||||
ui.serveFileTree(rw, req)
|
||||
case wantsClosure(req):
|
||||
case getSuffixMatches(req, closurePattern):
|
||||
ui.serveClosure(rw, req)
|
||||
case wantsReact(req):
|
||||
ui.serveReact(rw, req)
|
||||
case getSuffixMatches(req, reactPattern):
|
||||
ui.serveFromDiskOrStatic(rw, req, reactPattern, ui.fileReactHandler, reactstatic.Files)
|
||||
case getSuffixMatches(req, glitchPattern):
|
||||
ui.serveFromDiskOrStatic(rw, req, glitchPattern, ui.fileGlitchHandler, glitchstatic.Files)
|
||||
default:
|
||||
file := ""
|
||||
if m := staticFilePattern.FindStringSubmatch(suffix); m != nil {
|
||||
|
@ -570,20 +558,21 @@ func (ui *UIHandler) serveClosure(rw http.ResponseWriter, req *http.Request) {
|
|||
ui.closureHandler.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (ui *UIHandler) serveReact(rw http.ResponseWriter, req *http.Request) {
|
||||
// serveFromDiskOrStatic matches rx against req's path and serves the match either from disk (if non-nil) or from static (embedded in the binary).
|
||||
func (ui *UIHandler) serveFromDiskOrStatic(rw http.ResponseWriter, req *http.Request, rx *regexp.Regexp, disk http.Handler, static *fileembed.Files) {
|
||||
suffix := httputil.PathSuffix(req)
|
||||
m := reactPattern.FindStringSubmatch(suffix)
|
||||
m := rx.FindStringSubmatch(suffix)
|
||||
if m == nil {
|
||||
httputil.ErrorRouting(rw, req)
|
||||
return
|
||||
panic("Caller should verify that rx matches")
|
||||
}
|
||||
file := m[1]
|
||||
if ui.fileReactHandler != nil {
|
||||
if disk != nil {
|
||||
req.URL.Path = "/" + file
|
||||
ui.fileReactHandler.ServeHTTP(rw, req)
|
||||
disk.ServeHTTP(rw, req)
|
||||
} else {
|
||||
serveStaticFile(rw, req, reactstatic.Files, file)
|
||||
serveStaticFile(rw, req, static, file)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// serveDepsJS serves an auto-generated Closure deps.js file.
|
||||
|
|
|
@ -9,10 +9,17 @@ body {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-view-preview {
|
||||
.detail-view-img {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.detail-img-enter {
|
||||
transition: opacity 200ms linear;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.detail-img-enter.detail-img-enter-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.detail-view-sidebar {
|
||||
|
@ -24,6 +31,19 @@ body {
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.detail-view-piggy {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.detail-piggy-leave {
|
||||
transition: opacity 200ms linear;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.detail-piggy-leave.detail-piggy-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* http://www.paulirish.com/2012/box-sizing-border-box-ftw/ */
|
||||
*, *:before, *:after {
|
||||
-moz-box-sizing: border-box;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<script src="./deps.js"></script>
|
||||
<script src="?camli.mode=config&var=CAMLISTORE_CONFIG"></script>
|
||||
|
||||
<script type="text/javascript" src="react/react.js"></script>
|
||||
<script type="text/javascript" src="react/react-with-addons.js"></script>
|
||||
|
||||
<!-- Begin non-Closure cheating; but depended on by server_connection.js -->
|
||||
<script type="text/javascript" src="base64.js"></script>
|
||||
|
|
|
@ -16,72 +16,92 @@ limitations under the License.
|
|||
|
||||
goog.require('camlistore.AnimationLoop');
|
||||
goog.require('camlistore.ServerConnection');
|
||||
goog.require('SpritedAnimation');
|
||||
|
||||
goog.require('goog.math.Size');
|
||||
|
||||
function cached(fn) {
|
||||
var lastProps;
|
||||
var lastState;
|
||||
var lastVal;
|
||||
return function() {
|
||||
if (lastState == this.state && lastProps == this.props) {
|
||||
return lastVal;
|
||||
}
|
||||
lastProps = this.props;
|
||||
lastState = this.state;
|
||||
lastVal = fn.apply(this, arguments);
|
||||
return lastVal;
|
||||
}
|
||||
}
|
||||
goog.require('goog.object');
|
||||
|
||||
var DetailView = React.createClass({
|
||||
PREVIEW_MARGIN: 20,
|
||||
IMG_MARGIN: 20,
|
||||
PIGGY_WIDTH: 88,
|
||||
PIGGY_HEIGHT: 62,
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
description: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function(root) {
|
||||
var imageSize = 100; // We won't use this exact value; we only care about the aspect ratio.
|
||||
var connection = new camlistore.ServerConnection(this.props.config);
|
||||
connection.describeWithThumbnails(this.props.blobref, imageSize, function(description) {
|
||||
this.setState({
|
||||
this.setState({
|
||||
description: description
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getPreviewSize_: cached(function() {
|
||||
var meta = this.getPermanodeMeta_();
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
var aspect = new goog.math.Size(meta.thumbnailWidth, meta.thumbnailHeight);
|
||||
var available = new goog.math.Size(
|
||||
this.props.width - this.getSidebarWidth_() - this.PREVIEW_MARGIN * 2,
|
||||
this.props.height - this.PREVIEW_MARGIN * 2);
|
||||
return aspect.scaleToFit(available);
|
||||
}),
|
||||
|
||||
render: function() {
|
||||
var description = this.state ? this.state.description : '';
|
||||
this.imgSize_ = this.getImgSize_();
|
||||
return (
|
||||
React.DOM.div({className:'detail-view', style: this.getStyle_()},
|
||||
React.DOM.img({className:'detail-view-preview', key:'preview', src: this.getSrc_(), style: this.getPreviewStyle_()}),
|
||||
this.getImg_(),
|
||||
this.getPiggy_(),
|
||||
React.DOM.div({className:'detail-view-sidebar', key:'sidebar', style: this.getSidebarStyle_()},
|
||||
React.DOM.pre({key:'sidebar-pre'}, JSON.stringify(description, null, 2)))));
|
||||
React.DOM.pre({key:'sidebar-pre'}, JSON.stringify(this.state.description || '', null, 2)))));
|
||||
},
|
||||
|
||||
getImg_: function() {
|
||||
var transition = React.addons.TransitionGroup({transitionName: 'detail-img'}, []);
|
||||
if (this.state.description) {
|
||||
transition.props.children.push(
|
||||
React.DOM.img({
|
||||
className: 'detail-view-img',
|
||||
key: 'img',
|
||||
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.description) {
|
||||
transition.props.children.push(
|
||||
SpritedAnimation({
|
||||
src: 'glitch/npc_piggy__x1_walk_png_1354829432.png',
|
||||
className: 'detail-view-piggy',
|
||||
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() {
|
||||
if (!this.state) {
|
||||
// TODO(aa): Loading animation
|
||||
return '';
|
||||
}
|
||||
|
||||
var previewSize = this.getPreviewSize_();
|
||||
// Only re-request the image if we're increasing in size. Otherwise, let the browser resample.
|
||||
if (previewSize.height < (this.lastImageHeight || 0)) {
|
||||
if (this.imgSize_.height < (this.lastImageHeight || 0)) {
|
||||
console.log('Not re-requesting image becasue new size is smaller than existing...');
|
||||
} else {
|
||||
// If we re-request, ask for one bigger than we need right now, so that we're not constantly re-requesting as the browser resizes.
|
||||
this.lastImageHeight = previewSize.height * 1.25;
|
||||
this.lastImageHeight = this.imgSize_.height * 1.25;
|
||||
console.log('Requesting new image with size: ' + this.lastImageHeight);
|
||||
}
|
||||
|
||||
|
@ -90,6 +110,19 @@ var DetailView = React.createClass({
|
|||
return uri.toString();
|
||||
},
|
||||
|
||||
getImgSize_: function() {
|
||||
if (!this.state.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var meta = this.getPermanodeMeta_();
|
||||
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,
|
||||
|
@ -97,22 +130,6 @@ var DetailView = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
getPreviewStyle_: function() {
|
||||
if (!this.state || !this.getPreviewSize_().height) {
|
||||
return {
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
|
||||
var avail = new goog.math.Size(this.props.width - this.getSidebarWidth_(), this.props.height);
|
||||
return {
|
||||
top: (avail.height - this.getPreviewSize_().height) / 2,
|
||||
left: (avail.width - this.getPreviewSize_().width) / 2,
|
||||
width: this.getPreviewSize_().width,
|
||||
height: this.getPreviewSize_().height
|
||||
}
|
||||
},
|
||||
|
||||
getSidebarStyle_: function() {
|
||||
return {
|
||||
width: this.getSidebarWidth_()
|
||||
|
@ -124,9 +141,6 @@ var DetailView = React.createClass({
|
|||
},
|
||||
|
||||
getPermanodeMeta_: function() {
|
||||
if (!this.state) {
|
||||
return null;
|
||||
}
|
||||
return this.state.description.meta[this.props.blobref];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Object related utilities beyond what exist in Closure.
|
||||
*/
|
||||
goog.provide('object');
|
||||
|
||||
function extend(o, n) {
|
||||
var obj = {};
|
||||
goog.mixin(obj, o);
|
||||
goog.mixin(obj, n);
|
||||
return obj;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
goog.provide('SpritedAnimation');
|
||||
|
||||
goog.require('SpritedImage');
|
||||
goog.require('object');
|
||||
|
||||
var SpritedAnimation = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function(root) {
|
||||
this.timerId_ = window.setInterval(function() {
|
||||
this.setState({
|
||||
index: ++this.state.index % (this.props.sheetWidth * this.props.sheetHeight)
|
||||
})
|
||||
}.bind(this), this.props.interval);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.clearInterval(this.timerId_);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return SpritedImage(extend(this.props, {index: this.state.index}));
|
||||
}
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
goog.provide('SpritedImage');
|
||||
|
||||
goog.require('goog.object');
|
||||
goog.require('goog.string');
|
||||
|
||||
goog.require('object');
|
||||
|
||||
var SpritedImage = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div({className: this.props.className, style: extend(this.props.style, {overflow: 'hidden'})},
|
||||
React.DOM.img({src: this.props.src, style: this.getImgStyle_()})));
|
||||
},
|
||||
|
||||
getImgStyle_: function() {
|
||||
var x = this.props.index % this.props.sheetWidth;
|
||||
var y = Math.floor(this.props.index / this.props.sheetWidth);
|
||||
if (y >= this.props.sheetHeight) {
|
||||
throw new Error(goog.string.subs('Index %s out of range', this.props.index));
|
||||
}
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: -x * this.props.spriteWidth,
|
||||
top: -y * this.props.spriteHeight
|
||||
};
|
||||
}
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
The files in here come from www.glitchthegame.com.
|
||||
|
||||
License here: http://www.glitchthegame.com/public-domain-game-art/#licensing
|
||||
|
||||
All files are provided by Tiny Speck under the Creative Commons CC0 1.0
|
||||
Universal License. This is a broadly permissive "No Rights Reserved" license —
|
||||
you may do what you please with what we've provided. Our intention is to
|
||||
dedicate these works to the public domain and make them freely available to all,
|
||||
without restriction. All files are provided AS-IS. Tiny Speck cannot provide any
|
||||
support to help you bring these assets into your own projects.
|
||||
|
||||
Note: the Glitch logo and trademark are not among the things we are making
|
||||
available under this license. Only items in the files explicitly included herein
|
||||
are covered.
|
||||
|
||||
There is no obligation to link or credit the works, but if you do, please link
|
||||
to glitchthegame.com, our permanent "retirement" site for the game and these
|
||||
assets. Of course, links/shoutouts to Tiny Speck (tinyspeck.com) and/or Slack
|
||||
(slack.com) are appreciated.
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
#fileembed pattern .*\.png
|
||||
*/
|
||||
package glitch
|
||||
|
||||
import "camlistore.org/pkg/fileembed"
|
||||
|
||||
var Files = &fileembed.Files{}
|
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/*
|
||||
#fileembed pattern .*react(\.min)?\.js$
|
||||
#fileembed pattern .*\.js$
|
||||
*/
|
||||
package react
|
||||
|
||||
|
|
Loading…
Reference in New Issue