mirror of https://github.com/perkeep/perkeep.git
Merge branch 'master' of https://camlistore.googlesource.com/camlistore
This commit is contained in:
commit
4b2acfe155
|
@ -30,7 +30,7 @@ func TestMongoStorage(t *testing.T) {
|
||||||
// SetupMongoContainer may skip or fatal the test if docker isn't found or something goes wrong when setting up the container.
|
// SetupMongoContainer may skip or fatal the test if docker isn't found or something goes wrong when setting up the container.
|
||||||
// Thus, no error is returned
|
// Thus, no error is returned
|
||||||
containerID, ip := dockertest.SetupMongoContainer(t)
|
containerID, ip := dockertest.SetupMongoContainer(t)
|
||||||
defer dockertest.KillContainer(containerID)
|
defer containerID.Kill()
|
||||||
|
|
||||||
sto, err := newMongoStorage(config{
|
sto, err := newMongoStorage(config{
|
||||||
server: ip,
|
server: ip,
|
||||||
|
|
|
@ -19,7 +19,6 @@ package mysql_test
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -81,16 +80,8 @@ func newSorted(t *testing.T) (kv sorted.KeyValue, clean func()) {
|
||||||
do(rootdb, "DROP DATABASE IF EXISTS "+dbname)
|
do(rootdb, "DROP DATABASE IF EXISTS "+dbname)
|
||||||
do(rootdb, "CREATE DATABASE "+dbname)
|
do(rootdb, "CREATE DATABASE "+dbname)
|
||||||
|
|
||||||
db, err := sql.Open("mymysql", dbname+"/root/root")
|
kv, err := mysql.NewKeyValue(mysql.Config{
|
||||||
if err != nil {
|
Host: "localhost:3306",
|
||||||
t.Fatalf("opening test database: " + err.Error())
|
|
||||||
}
|
|
||||||
for _, tableSql := range mysql.SQLCreateTables() {
|
|
||||||
do(db, tableSql)
|
|
||||||
}
|
|
||||||
do(db, fmt.Sprintf(`REPLACE INTO meta VALUES ('version', '%d')`, mysql.SchemaVersion()))
|
|
||||||
|
|
||||||
kv, err = mysql.NewKeyValue(mysql.Config{
|
|
||||||
Database: dbname,
|
Database: dbname,
|
||||||
User: "root",
|
User: "root",
|
||||||
Password: "root",
|
Password: "root",
|
||||||
|
|
|
@ -23,21 +23,19 @@ import (
|
||||||
"camlistore.org/pkg/test/dockertest"
|
"camlistore.org/pkg/test/dockertest"
|
||||||
)
|
)
|
||||||
|
|
||||||
const mongoImage = "robinvdvleuten/mongo"
|
|
||||||
|
|
||||||
// TestMongoKV tests against a real MongoDB instance, using a Docker container.
|
// TestMongoKV tests against a real MongoDB instance, using a Docker container.
|
||||||
// Currently using https://index.docker.io/u/robinvdvleuten/mongo/
|
|
||||||
func TestMongoKV(t *testing.T) {
|
func TestMongoKV(t *testing.T) {
|
||||||
// SetupMongoContainer may skip or fatal the test if docker isn't found or something goes wrong when setting up the container.
|
// SetupMongoContainer may skip or fatal the test if docker isn't found or something goes wrong when setting up the container.
|
||||||
// Thus, no error is returned
|
// Thus, no error is returned
|
||||||
containerID, ip := dockertest.SetupMongoContainer(t)
|
containerID, ip := dockertest.SetupMongoContainer(t)
|
||||||
defer dockertest.KillContainer(containerID)
|
defer containerID.Kill()
|
||||||
|
|
||||||
kv, err := NewKeyValue(Config{
|
kv, err := NewKeyValue(Config{
|
||||||
Server: ip,
|
Server: ip,
|
||||||
Database: "camlitest",
|
Database: "camlitest",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("monogo.NewKeyValue = %v", err)
|
t.Fatalf("mongo.NewKeyValue = %v", err)
|
||||||
}
|
}
|
||||||
kvtest.TestSorted(t, kv)
|
kvtest.TestSorted(t, kv)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,12 @@ func SchemaVersion() int {
|
||||||
// which is purely about bytes.
|
// which is purely about bytes.
|
||||||
func SQLCreateTables() []string {
|
func SQLCreateTables() []string {
|
||||||
return []string{
|
return []string{
|
||||||
`CREATE TABLE rows (
|
`CREATE TABLE IF NOT EXISTS rows (
|
||||||
k VARCHAR(255) NOT NULL PRIMARY KEY,
|
k VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||||
v VARCHAR(255))
|
v VARCHAR(255))
|
||||||
DEFAULT CHARACTER SET binary`,
|
DEFAULT CHARACTER SET binary`,
|
||||||
|
|
||||||
`CREATE TABLE meta (
|
`CREATE TABLE IF NOT EXISTS meta (
|
||||||
metakey VARCHAR(255) NOT NULL PRIMARY KEY,
|
metakey VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||||
value VARCHAR(255) NOT NULL)
|
value VARCHAR(255) NOT NULL)
|
||||||
DEFAULT CHARACTER SET binary`,
|
DEFAULT CHARACTER SET binary`,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"camlistore.org/pkg/jsonconfig"
|
"camlistore.org/pkg/jsonconfig"
|
||||||
"camlistore.org/pkg/sorted"
|
"camlistore.org/pkg/sorted"
|
||||||
|
@ -36,7 +37,11 @@ func init() {
|
||||||
|
|
||||||
// Config holds the parameters used to connect to the MySQL db.
|
// Config holds the parameters used to connect to the MySQL db.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Host string // Optional. Defaults to "localhost" in ConfigFromJSON.
|
// Host optionally specifies the address on which mysqld listens. It should
|
||||||
|
// be of the form hostname:port, or addr:port. If empty, a local connection
|
||||||
|
// will be used. If the address does not have a colon, it is assumed the
|
||||||
|
// port is missing and the default MySQL (3306) one will be set in ConfigFromJSON.
|
||||||
|
Host string
|
||||||
Database string // Required.
|
Database string // Required.
|
||||||
User string // Required.
|
User string // Required.
|
||||||
Password string // Optional.
|
Password string // Optional.
|
||||||
|
@ -45,8 +50,14 @@ type Config struct {
|
||||||
// ConfigFromJSON populates Config from config, and validates
|
// ConfigFromJSON populates Config from config, and validates
|
||||||
// config. It returns an error if config fails to validate.
|
// config. It returns an error if config fails to validate.
|
||||||
func ConfigFromJSON(config jsonconfig.Obj) (Config, error) {
|
func ConfigFromJSON(config jsonconfig.Obj) (Config, error) {
|
||||||
|
host := config.OptionalString("host", "")
|
||||||
|
if host != "" {
|
||||||
|
if !strings.Contains(host, ":") {
|
||||||
|
host = host + ":3306"
|
||||||
|
}
|
||||||
|
}
|
||||||
conf := Config{
|
conf := Config{
|
||||||
Host: config.OptionalString("host", "localhost"),
|
Host: host,
|
||||||
User: config.RequiredString("user"),
|
User: config.RequiredString("user"),
|
||||||
Password: config.OptionalString("password", ""),
|
Password: config.OptionalString("password", ""),
|
||||||
Database: config.RequiredString("database"),
|
Database: config.RequiredString("database"),
|
||||||
|
@ -67,14 +78,23 @@ func newKeyValueFromJSONConfig(cfg jsonconfig.Obj) (sorted.KeyValue, error) {
|
||||||
|
|
||||||
// NewKeyValue returns a sorted.KeyValue implementation of the described MySQL database.
|
// NewKeyValue returns a sorted.KeyValue implementation of the described MySQL database.
|
||||||
func NewKeyValue(cfg Config) (sorted.KeyValue, error) {
|
func NewKeyValue(cfg Config) (sorted.KeyValue, error) {
|
||||||
// TODO(bradfitz,mpl): host is ignored for now. I think we can connect to host with:
|
|
||||||
// tcp:ADDR*DBNAME/USER/PASSWD (http://godoc.org/github.com/ziutek/mymysql/godrv#Driver.Open)
|
|
||||||
// I suppose we'll have to do a lookup first.
|
|
||||||
dsn := cfg.Database + "/" + cfg.User + "/" + cfg.Password
|
dsn := cfg.Database + "/" + cfg.User + "/" + cfg.Password
|
||||||
|
if cfg.Host != "" {
|
||||||
|
dsn = "tcp:" + cfg.Host + "*" + dsn
|
||||||
|
}
|
||||||
db, err := sql.Open("mymysql", dsn)
|
db, err := sql.Open("mymysql", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for _, tableSql := range SQLCreateTables() {
|
||||||
|
if _, err := db.Exec(tableSql); err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating table with %q: %v", tableSql, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(fmt.Sprintf(`REPLACE INTO meta VALUES ('version', '%d')`, SchemaVersion())); err != nil {
|
||||||
|
return nil, fmt.Errorf("error setting schema version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
kv := &keyValue{
|
kv := &keyValue{
|
||||||
db: db,
|
db: db,
|
||||||
KeyValue: &sqlkv.KeyValue{
|
KeyValue: &sqlkv.KeyValue{
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2014 The Camlistore Authors
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"camlistore.org/pkg/osutil"
|
||||||
|
"camlistore.org/pkg/sorted/kvtest"
|
||||||
|
"camlistore.org/pkg/test/dockertest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMySQLKV tests against a real MySQL instance, using a Docker container.
|
||||||
|
func TestMySQLKV(t *testing.T) {
|
||||||
|
dbname := "camlitest_" + osutil.Username()
|
||||||
|
containerID, ip := dockertest.SetupMySQLContainer(t, dbname)
|
||||||
|
defer containerID.Kill()
|
||||||
|
|
||||||
|
kv, err := NewKeyValue(Config{
|
||||||
|
Host: ip + ":3306",
|
||||||
|
Database: dbname,
|
||||||
|
User: "root",
|
||||||
|
Password: "root",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mysql.NewKeyValue = %v", err)
|
||||||
|
}
|
||||||
|
kvtest.TestSorted(t, kv)
|
||||||
|
}
|
|
@ -24,11 +24,42 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"camlistore.org/pkg/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HaveImage(name string) (ok bool, err error) {
|
/// runLongTest checks all the conditions for running a docker container
|
||||||
|
// based on image.
|
||||||
|
func runLongTest(t *testing.T, image string) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping in short mode")
|
||||||
|
}
|
||||||
|
if !haveDocker() {
|
||||||
|
t.Skip("skipping test; 'docker' command not found")
|
||||||
|
}
|
||||||
|
if ok, err := haveImage(image); !ok || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Error running docker to check for %s: %v", image, err)
|
||||||
|
}
|
||||||
|
log.Printf("Pulling docker image %s ...", image)
|
||||||
|
if err := Pull(image); err != nil {
|
||||||
|
t.Skipf("Error pulling %s: %v", image, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// haveDocker returns whether the "docker" command was found.
|
||||||
|
func haveDocker() bool {
|
||||||
|
_, err := exec.LookPath("docker")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveImage(name string) (ok bool, err error) {
|
||||||
out, err := exec.Command("docker", "images", "--no-trunc").Output()
|
out, err := exec.Command("docker", "images", "--no-trunc").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -36,12 +67,15 @@ func HaveImage(name string) (ok bool, err error) {
|
||||||
return bytes.Contains(out, []byte(name)), nil
|
return bytes.Contains(out, []byte(name)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(args ...string) (containerID string, err error) {
|
func run(args ...string) (containerID string, err error) {
|
||||||
runOut, err := exec.Command("docker", append([]string{"run"}, args...)...).Output()
|
cmd := exec.Command("docker", append([]string{"run"}, args...)...)
|
||||||
if err != nil {
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||||
|
if err = cmd.Run(); err != nil {
|
||||||
|
err = fmt.Errorf("%v%v", stderr.String(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
containerID = strings.TrimSpace(string(runOut))
|
containerID = strings.TrimSpace(stdout.String())
|
||||||
if containerID == "" {
|
if containerID == "" {
|
||||||
return "", errors.New("unexpected empty output from `docker run`")
|
return "", errors.New("unexpected empty output from `docker run`")
|
||||||
}
|
}
|
||||||
|
@ -52,14 +86,16 @@ func KillContainer(container string) error {
|
||||||
return exec.Command("docker", "kill", container).Run()
|
return exec.Command("docker", "kill", container).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Pull(name string) error {
|
// Pull retrieves the docker image with 'docker pull'.
|
||||||
out, err := exec.Command("docker", "pull", name).CombinedOutput()
|
func Pull(image string) error {
|
||||||
|
out, err := exec.Command("docker", "pull", image).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%v: %s", err, out)
|
err = fmt.Errorf("%v: %s", err, out)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IP returns the IP address of the container.
|
||||||
func IP(containerID string) (string, error) {
|
func IP(containerID string) (string, error) {
|
||||||
out, err := exec.Command("docker", "inspect", containerID).Output()
|
out, err := exec.Command("docker", "inspect", containerID).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -81,5 +117,75 @@ func IP(containerID string) (string, error) {
|
||||||
if ip := c[0].NetworkSettings.IPAddress; ip != "" {
|
if ip := c[0].NetworkSettings.IPAddress; ip != "" {
|
||||||
return ip, nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
return "", errors.New("no IP. Not running?")
|
return "", fmt.Errorf("could not find an IP for %v. Not running?", containerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerID string
|
||||||
|
|
||||||
|
func (c ContainerID) IP() (string, error) {
|
||||||
|
return IP(string(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ContainerID) Kill() error {
|
||||||
|
return KillContainer(string(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup retrieves the ip address of the container, and tries to reach
|
||||||
|
// before timeout the tcp address at this ip and given port.
|
||||||
|
func (c ContainerID) lookup(port int, timeout time.Duration) (ip string, err error) {
|
||||||
|
ip, err = c.IP()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error getting container IP: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||||
|
if err = netutil.AwaitReachable(addr, timeout); err != nil {
|
||||||
|
err = fmt.Errorf("timeout trying to reach %s for container %v: %v", addr, c, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
mongoImage = "robinvdvleuten/mongo"
|
||||||
|
mysqlImage = "orchardup/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupMongoContainer sets up a real MongoDB instance for testing purposes,
|
||||||
|
// using a Docker container. It returns the container ID and its IP address,
|
||||||
|
// or makes the test fail on error.
|
||||||
|
// Currently using https://index.docker.io/u/robinvdvleuten/mongo/
|
||||||
|
func SetupMongoContainer(t *testing.T) (c ContainerID, ip string) {
|
||||||
|
runLongTest(t, mongoImage)
|
||||||
|
|
||||||
|
containerID, err := run("-d", mongoImage, "--smallfiles")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("docker run: %v", err)
|
||||||
|
}
|
||||||
|
c = ContainerID(containerID)
|
||||||
|
ip, err = c.lookup(27017, 20*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
c.Kill()
|
||||||
|
t.Fatalf("container lookup: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupMySQLContainer sets up a real MySQL instance for testing purposes,
|
||||||
|
// using a Docker container. It returns the container ID and its IP address,
|
||||||
|
// or makes the test fail on error.
|
||||||
|
// Currently using https://index.docker.io/u/orchardup/mysql/
|
||||||
|
func SetupMySQLContainer(t *testing.T, dbname string) (c ContainerID, ip string) {
|
||||||
|
runLongTest(t, mongoImage)
|
||||||
|
|
||||||
|
containerID, err := run("-d", "-e", "MYSQL_ROOT_PASSWORD=root", "-e", "MYSQL_DATABASE="+dbname, mysqlImage)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("docker run: %v", err)
|
||||||
|
}
|
||||||
|
c = ContainerID(containerID)
|
||||||
|
ip, err = c.lookup(3306, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
c.Kill()
|
||||||
|
t.Fatalf("container lookup: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2014 The Camlistore Authors
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dockertest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"camlistore.org/pkg/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const mongoImage = "robinvdvleuten/mongo"
|
|
||||||
|
|
||||||
// SetupMongoContainer sets up a real MongoDB instance for testing purposes, using a Docker container.
|
|
||||||
// Currently using https://index.docker.io/u/robinvdvleuten/mongo/
|
|
||||||
func SetupMongoContainer(t *testing.T) (containerID, ip string) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping in short mode")
|
|
||||||
}
|
|
||||||
if _, err := exec.LookPath("docker"); err != nil {
|
|
||||||
t.Skip("skipping without docker available in path")
|
|
||||||
}
|
|
||||||
if ok, err := HaveImage(mongoImage); !ok || err != nil {
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("Error running docker to check for %s: %v", mongoImage, err)
|
|
||||||
}
|
|
||||||
log.Printf("Pulling docker image %s ...", mongoImage)
|
|
||||||
if err := Pull(mongoImage); err != nil {
|
|
||||||
t.Skipf("Error pulling %s: %v", mongoImage, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
containerID, err = Run("-d", mongoImage, "--smallfiles")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("docker run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err = IP(containerID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error getting container IP: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := netutil.AwaitReachable(ip+":27017", 20*time.Second); err != nil {
|
|
||||||
t.Fatal("timeout waiting for port to become reachable")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* TODO(aa): All this needs to get renamed to image-detail */
|
||||||
.detail-view {
|
.detail-view {
|
||||||
background: black;
|
background: black;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -24,6 +24,7 @@ goog.require('goog.string');
|
||||||
|
|
||||||
goog.require('cam.AnimationLoop');
|
goog.require('cam.AnimationLoop');
|
||||||
goog.require('cam.BlobItemReactData');
|
goog.require('cam.BlobItemReactData');
|
||||||
|
goog.require('cam.ImageDetail');
|
||||||
goog.require('cam.imageUtil');
|
goog.require('cam.imageUtil');
|
||||||
goog.require('cam.Navigator');
|
goog.require('cam.Navigator');
|
||||||
goog.require('cam.reactUtil');
|
goog.require('cam.reactUtil');
|
||||||
|
@ -34,10 +35,6 @@ goog.require('cam.SpritedAnimation');
|
||||||
cam.DetailView = React.createClass({
|
cam.DetailView = React.createClass({
|
||||||
displayName: 'DetailView',
|
displayName: 'DetailView',
|
||||||
|
|
||||||
IMG_MARGIN: 20,
|
|
||||||
PIGGY_WIDTH: 88,
|
|
||||||
PIGGY_HEIGHT: 62,
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
blobref: React.PropTypes.string.isRequired,
|
blobref: React.PropTypes.string.isRequired,
|
||||||
getDetailURL: React.PropTypes.func.isRequired,
|
getDetailURL: React.PropTypes.func.isRequired,
|
||||||
|
@ -52,25 +49,15 @@ cam.DetailView = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
this.imgSize_ = null;
|
|
||||||
this.lastImageHeight_ = 0;
|
|
||||||
this.pendingNavigation_ = 0;
|
|
||||||
this.navCount_ = 1;
|
|
||||||
this.eh_ = new goog.events.EventHandler(this);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imgHasLoaded: false,
|
lastNavigateWasBackward: false,
|
||||||
backwardPiggy: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillMount: function() {
|
||||||
if (this.props.blobref != nextProps.blobref) {
|
this.pendingNavigation_ = 0;
|
||||||
this.blobItemData_ = null;
|
this.navCount_ = 1;
|
||||||
this.imgSize_ = null;
|
this.eh_ = new goog.events.EventHandler(this);
|
||||||
this.lastImageHeight_ = 0;
|
|
||||||
this.setState({imgHasLoaded: false});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function(root) {
|
componentDidMount: function(root) {
|
||||||
|
@ -79,103 +66,21 @@ cam.DetailView = React.createClass({
|
||||||
this.searchUpdated_();
|
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_);
|
|
||||||
img.getDOMNode().addEventListener('error', function() {
|
|
||||||
console.error('Could not load image: %s', img.props.src);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
this.blobItemData_ = this.getBlobItemData_();
|
if (!this.dataIsLoaded_()) {
|
||||||
this.imgSize_ = this.getImgSize_();
|
return React.DOM.div();
|
||||||
return React.DOM.div({className:'detail-view', style: this.getStyle_()}, [
|
|
||||||
this.getImg_(),
|
|
||||||
this.getPiggy_(),
|
|
||||||
this.getSidebar_(),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
getSidebar_: function() {
|
|
||||||
var children = !this.blobItemData_ ? [] : [
|
|
||||||
this.getGeneralProperties_(),
|
|
||||||
this.getFileishProperties_(),
|
|
||||||
this.getImageProperties_(),
|
|
||||||
this.getNavProperties_(),
|
|
||||||
];
|
|
||||||
return cam.PropertySheetContainer({className:'detail-view-sidebar', style:this.getSidebarStyle_()}, children);
|
|
||||||
},
|
|
||||||
|
|
||||||
getGeneralProperties_: function() {
|
|
||||||
if (this.blobItemData_.m.camliType != 'permanode') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return cam.PropertySheet({key:'general', title:'Generalities'}, [
|
|
||||||
React.DOM.h1({className:'detail-title'}, this.getSinglePermanodeAttr_('title') || '<no title>'),
|
|
||||||
React.DOM.p({className:'detail-description'}, this.getSinglePermanodeAttr_('description') || '<no description>'),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
getFileishProperties_: function() {
|
|
||||||
var isFile = this.blobItemData_.rm.camliType == 'file';
|
|
||||||
var isDir = this.blobItemData_.rm.camliType == 'directory';
|
|
||||||
if (!isFile && !isDir) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return cam.PropertySheet({className:'detail-fileish-properties', key:'file', title: isFile ? 'File' : 'Directory'}, [
|
|
||||||
React.DOM.table({}, [
|
|
||||||
React.DOM.tr({}, [
|
|
||||||
React.DOM.td({}, 'filename'),
|
|
||||||
React.DOM.td({}, isFile ? this.blobItemData_.rm.file.fileName : this.blobItemData_.rm.dir.fileName),
|
|
||||||
]),
|
|
||||||
React.DOM.tr({}, [
|
|
||||||
React.DOM.td({}, 'size'),
|
|
||||||
React.DOM.td({}, this.blobItemData_.rm.file.size + ' bytes'), // TODO(aa): Humanize units
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
getImageProperties_: function() {
|
|
||||||
if (!this.blobItemData_.im) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cam.PropertySheet({className:'detail-image-properties', key:'image', title: 'Image'}, [
|
// TODO(aa): Different types of detail views can go here based on what's in blobItemData.
|
||||||
React.DOM.table({}, [
|
return cam.ImageDetail({
|
||||||
React.DOM.tr({}, [
|
backwardPiggy: this.state.lastNavigateWasBackward,
|
||||||
React.DOM.td({}, 'width'),
|
blobItemData: new cam.BlobItemReactData(this.props.blobref, this.props.searchSession.getCurrentResults().description.meta),
|
||||||
React.DOM.td({}, this.blobItemData_.im.width),
|
height: this.props.height,
|
||||||
]),
|
oldURL: this.props.oldURL,
|
||||||
React.DOM.tr({}, [
|
onEscape: this.handleEscape_,
|
||||||
React.DOM.td({}, 'height'),
|
searchURL: this.props.searchURL,
|
||||||
React.DOM.td({}, this.blobItemData_.im.height),
|
width: this.props.width,
|
||||||
]),
|
});
|
||||||
// TODO(aa): encoding type, exif data, etc.
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
getNavProperties_: function() {
|
|
||||||
return cam.PropertySheet({key:'nav', title:'Elsewhere'}, [
|
|
||||||
React.DOM.a({key:'search-link', href:this.props.searchURL.toString(), onClick:this.handleEscape_}, 'Back to search'),
|
|
||||||
React.DOM.br(),
|
|
||||||
React.DOM.a({key:'old-link', href:this.props.oldURL.toString()}, 'Old (editable) UI'),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO(aa): We need a Permanode utility class that wraps the JSON goop.
|
|
||||||
getSinglePermanodeAttr_: function(name) {
|
|
||||||
var m = this.blobItemData_.m;
|
|
||||||
if (m.camliType == 'permanode' && m.permanode.attr[name]) {
|
|
||||||
return goog.isArray(m.permanode.attr[name]) ? m.permanode.attr[name][0] : m.permanode.attr[name];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -195,7 +100,7 @@ cam.DetailView = React.createClass({
|
||||||
navigate_: function(offset) {
|
navigate_: function(offset) {
|
||||||
this.pendingNavigation_ = offset;
|
this.pendingNavigation_ = offset;
|
||||||
++this.navCount_;
|
++this.navCount_;
|
||||||
this.setState({backwardPiggy: offset < 0});
|
this.setState({lastNavigateWasBackward: offset < 0});
|
||||||
this.handlePendingNavigation_();
|
this.handlePendingNavigation_();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -240,15 +145,10 @@ cam.DetailView = React.createClass({
|
||||||
this.props.navigator.navigate(this.props.getDetailURL(results.blobs[index].blob));
|
this.props.navigator.navigate(this.props.getDetailURL(results.blobs[index].blob));
|
||||||
},
|
},
|
||||||
|
|
||||||
onImgLoad_: function() {
|
|
||||||
this.setState({imgHasLoaded:true});
|
|
||||||
},
|
|
||||||
|
|
||||||
searchUpdated_: function() {
|
searchUpdated_: function() {
|
||||||
this.handlePendingNavigation_();
|
this.handlePendingNavigation_();
|
||||||
|
|
||||||
this.blobItemData_ = this.getBlobItemData_();
|
if (this.dataIsLoaded_()) {
|
||||||
if (this.blobItemData_) {
|
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -265,113 +165,7 @@ cam.DetailView = React.createClass({
|
||||||
this.props.searchSession.loadMoreResults();
|
this.props.searchSession.loadMoreResults();
|
||||||
},
|
},
|
||||||
|
|
||||||
getImg_: function() {
|
dataIsLoaded_: function() {
|
||||||
var transition = React.addons.TransitionGroup({transitionName: 'detail-img'}, []);
|
return Boolean(this.props.searchSession.getCurrentResults().description.meta[this.props.blobref]);
|
||||||
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_ = Math.min(this.blobItemData_.im.height, cam.imageUtil.getSizeToRequest(this.imgSize_.height, this.lastImageHeight_));
|
|
||||||
return this.blobItemData_.getThumbSrc(this.lastImageHeight_);
|
|
||||||
},
|
|
||||||
|
|
||||||
getImgSize_: function() {
|
|
||||||
if (!this.blobItemData_ || !this.blobItemData_.im) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var rawSize = new goog.math.Size(this.blobItemData_.im.width, this.blobItemData_.im.height);
|
|
||||||
var available = new goog.math.Size(
|
|
||||||
this.props.width - this.getSidebarWidth_() - this.IMG_MARGIN * 2,
|
|
||||||
this.props.height - this.IMG_MARGIN * 2);
|
|
||||||
if (rawSize.height <= available.height && rawSize.width <= available.width) {
|
|
||||||
return rawSize;
|
|
||||||
}
|
|
||||||
return rawSize.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() {
|
|
||||||
if (!this.blobItemData_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.blobItemData_.m;
|
|
||||||
},
|
|
||||||
|
|
||||||
getBlobItemData_: function() {
|
|
||||||
var metabag = this.props.searchSession.getCurrentResults().description.meta;
|
|
||||||
if (!metabag[this.props.blobref]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new cam.BlobItemReactData(this.props.blobref, metabag);
|
|
||||||
},
|
|
||||||
|
|
||||||
getImageRef_: function() {
|
|
||||||
return this.refs && this.refs[this.getImageId_()];
|
|
||||||
},
|
|
||||||
|
|
||||||
getImageId_: function() {
|
|
||||||
return 'img' + this.props.blobref;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
/*
|
||||||
|
Copyright 2014 The Camlistore Authors
|
||||||
|
|
||||||
|
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.ImageDetail');
|
||||||
|
|
||||||
|
cam.ImageDetail = React.createClass({
|
||||||
|
displayName: 'ImageDetail',
|
||||||
|
|
||||||
|
IMG_MARGIN: 20,
|
||||||
|
PIGGY_WIDTH: 88,
|
||||||
|
PIGGY_HEIGHT: 62,
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
backwardPiggy: false,
|
||||||
|
blobItemData: React.PropTypes.instanceOf(cam.BlobItemReactData).isRequired,
|
||||||
|
height: React.PropTypes.number.isRequired,
|
||||||
|
oldURL: React.PropTypes.instanceOf(goog.Uri).isRequired,
|
||||||
|
onEscape: React.PropTypes.func.isRequired,
|
||||||
|
searchURL: React.PropTypes.instanceOf(goog.Uri).isRequired,
|
||||||
|
width: React.PropTypes.number.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
this.imgSize_ = null;
|
||||||
|
this.lastImageHeight_ = 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
imgHasLoaded: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
if (this.props.blobItemData.blobref != nextProps.blobItemData.blobref) {
|
||||||
|
this.imgSize_ = null;
|
||||||
|
this.lastImageHeight_ = 0;
|
||||||
|
this.setState({imgHasLoaded: false});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.componentDidUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function() {
|
||||||
|
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_);
|
||||||
|
img.getDOMNode().addEventListener('error', function() {
|
||||||
|
console.error('Could not load image: %s', img.props.src);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
this.imgSize_ = this.getImgSize_();
|
||||||
|
return React.DOM.div({className:'detail-view', style: this.getStyle_()}, [
|
||||||
|
this.getImg_(),
|
||||||
|
this.getPiggy_(),
|
||||||
|
this.getSidebar_(),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSidebar_: function() {
|
||||||
|
return cam.PropertySheetContainer({className:'detail-view-sidebar', style:this.getSidebarStyle_()}, [
|
||||||
|
this.getGeneralProperties_(),
|
||||||
|
this.getFileishProperties_(),
|
||||||
|
this.getImageProperties_(),
|
||||||
|
this.getNavProperties_(),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getGeneralProperties_: function() {
|
||||||
|
if (this.props.blobItemData.m.camliType != 'permanode') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cam.PropertySheet({key:'general', title:'Generalities'}, [
|
||||||
|
React.DOM.h1({className:'detail-title'}, this.getSinglePermanodeAttr_('title') || '<no title>'),
|
||||||
|
React.DOM.p({className:'detail-description'}, this.getSinglePermanodeAttr_('description') || '<no description>'),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileishProperties_: function() {
|
||||||
|
var isFile = this.props.blobItemData.rm.camliType == 'file';
|
||||||
|
var isDir = this.props.blobItemData.rm.camliType == 'directory';
|
||||||
|
if (!isFile && !isDir) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cam.PropertySheet({className:'detail-fileish-properties', key:'file', title: isFile ? 'File' : 'Directory'}, [
|
||||||
|
React.DOM.table({}, [
|
||||||
|
React.DOM.tr({}, [
|
||||||
|
React.DOM.td({}, 'filename'),
|
||||||
|
React.DOM.td({}, isFile ? this.props.blobItemData.rm.file.fileName : this.props.blobItemData.rm.dir.fileName),
|
||||||
|
]),
|
||||||
|
React.DOM.tr({}, [
|
||||||
|
React.DOM.td({}, 'size'),
|
||||||
|
React.DOM.td({}, this.props.blobItemData.rm.file.size + ' bytes'), // TODO(aa): Humanize units
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getImageProperties_: function() {
|
||||||
|
if (!this.props.blobItemData.im) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cam.PropertySheet({className:'detail-image-properties', key:'image', title: 'Image'}, [
|
||||||
|
React.DOM.table({}, [
|
||||||
|
React.DOM.tr({}, [
|
||||||
|
React.DOM.td({}, 'width'),
|
||||||
|
React.DOM.td({}, this.props.blobItemData.im.width),
|
||||||
|
]),
|
||||||
|
React.DOM.tr({}, [
|
||||||
|
React.DOM.td({}, 'height'),
|
||||||
|
React.DOM.td({}, this.props.blobItemData.im.height),
|
||||||
|
]),
|
||||||
|
// TODO(aa): encoding type, exif data, etc.
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
getNavProperties_: function() {
|
||||||
|
return cam.PropertySheet({key:'nav', title:'Elsewhere'}, [
|
||||||
|
React.DOM.a({key:'search-link', href:this.props.searchURL.toString(), onClick:this.props.onEscape}, 'Back to search'),
|
||||||
|
React.DOM.br(),
|
||||||
|
React.DOM.a({key:'old-link', href:this.props.oldURL.toString()}, 'Old (editable) UI'),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO(aa): We need a Permanode utility class that wraps the JSON goop.
|
||||||
|
getSinglePermanodeAttr_: function(name) {
|
||||||
|
var m = this.props.blobItemData.m;
|
||||||
|
if (m.camliType == 'permanode' && m.permanode.attr[name]) {
|
||||||
|
return goog.isArray(m.permanode.attr[name]) ? m.permanode.attr[name][0] : m.permanode.attr[name];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onImgLoad_: function() {
|
||||||
|
this.setState({imgHasLoaded:true});
|
||||||
|
},
|
||||||
|
|
||||||
|
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.props.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_ = Math.min(this.props.blobItemData.im.height, cam.imageUtil.getSizeToRequest(this.imgSize_.height, this.lastImageHeight_));
|
||||||
|
return this.props.blobItemData.getThumbSrc(this.lastImageHeight_);
|
||||||
|
},
|
||||||
|
|
||||||
|
getImgSize_: function() {
|
||||||
|
if (!this.props.blobItemData || !this.props.blobItemData.im) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var rawSize = new goog.math.Size(this.props.blobItemData.im.width, this.props.blobItemData.im.height);
|
||||||
|
var available = new goog.math.Size(
|
||||||
|
this.props.width - this.getSidebarWidth_() - this.IMG_MARGIN * 2,
|
||||||
|
this.props.height - this.IMG_MARGIN * 2);
|
||||||
|
if (rawSize.height <= available.height && rawSize.width <= available.width) {
|
||||||
|
return rawSize;
|
||||||
|
}
|
||||||
|
return rawSize.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() {
|
||||||
|
if (!this.props.blobItemData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.props.blobItemData.m;
|
||||||
|
},
|
||||||
|
|
||||||
|
getImageRef_: function() {
|
||||||
|
return this.refs && this.refs[this.getImageId_()];
|
||||||
|
},
|
||||||
|
|
||||||
|
getImageId_: function() {
|
||||||
|
return 'img' + this.props.blobItemData.blobref;
|
||||||
|
}
|
||||||
|
});
|
|
@ -51,4 +51,5 @@ Or browse at Github: <a href="https://github.com/bradfitz/camlistore/tree/0.8">g
|
||||||
</ul>
|
</ul>
|
||||||
<h3>General</h3>
|
<h3>General</h3>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>kv: all the verifydb flags are on by default on dev, to help with detecting corruptions.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Reference in New Issue