package org.picasaapp.client;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.picasaapp.client.PicasaWeb.*;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.i18n.client.Dictionary;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;
/**
* The entry point class of the application.
*/
public class PicasaApp implements EntryPoint,
ValueChangeHandler<java.lang.String> {
/**
* A node in an album tree.
*/
private static class AlbumTreeNode {
private Album album;
private List<AlbumTreeNode> children = new ArrayList<AlbumTreeNode>();
public AlbumTreeNode(Album album) {
this.album = album;
}
public Album getAlbum() { return album; }
public void addChild(AlbumTreeNode child) {
children.add(child);
}
public List<AlbumTreeNode> getChildren() {
return children;
}
}
/**
* A remote service proxy to talk to the server-side album tree service.
*/
private final AlbumLinkServiceAsync albumLinkService = GWT
.create(AlbumLinkService.class);
/** This tree represents the album hierarchy. */
private Tree tree;
/** This label contains the error message. */
private Label errorLabel;
/** This panel contains all the thumbnails. */
private RootPanel thumbnails;
/** The name of the current user. */
private String username = "";
/** Show username in URL. This is for the case when the application is used
* for displaying albums of multiple users. */
private boolean showUsername;
/** The name of the current album. */
private String albumName;
/** The JavaScript overlay object for the Picasa albums JSON response. */
private Albums albums;
/** The JavaScript overlay object for the Picasa photos JSON response. */
private Photos photos;
/** The index of currently displayed photo in {@code photos}. */
private int photoIndex;
/** Set window title according to the current location. */
private boolean setWindowTitle;
/** Specifies whether to load the album links. */
private boolean loadAlbumLinks;
/** The links to the parent albums. */
private AlbumLink[] links;
/** The roots of album trees. */
private List<AlbumTreeNode> roots;
private void setErrorMessage(String message) {
errorLabel.setText(message);
}
private String getLocation(String albumName) {
return getLocation(albumName, null);
}
private String getLocation(String albumName, String photoID) {
String location = "";
if (showUsername) location += username;
if (albumName != null) {
if (!location.isEmpty()) location += '/';
location += albumName;
if (photoID != null) location += '/' + photoID;
}
return location;
}
private void setWindowTitle(String albumTitle) {
if (!setWindowTitle) return;
String title = albums.getAuthorName() + " - Picasa";
if (albumTitle != null) title = albumTitle + " - " + title;
Window.setTitle(title);
}
/**
* Builds the album trees.
*/
private void buildTrees() {
try {
Map<String, AlbumTreeNode> albumIDToNode =
new LinkedHashMap<String, AlbumTreeNode>();
for (int i = 0; i < albums.getNumAlbums(); ++i) {
final Album album = albums.getAlbum(i);
albumIDToNode.put(album.getID(), new AlbumTreeNode(album));
}
if (links != null) {
for (AlbumLink link: links) {
AlbumTreeNode child = albumIDToNode.get(link.getAlbumID());
AlbumTreeNode parent =
albumIDToNode.get(link.getParentAlbumID());
parent.addChild(child);
albumIDToNode.remove(link.getAlbumID());
}
}
roots = new ArrayList<AlbumTreeNode>(albumIDToNode.size());
for (AlbumTreeNode node: albumIDToNode.values())
roots.add(node);
}
finally {
links = null;
}
}
/**
* Adds an album thumbnail to the panel.
*/
public void addAlbumThumbnail(Album album) {
Image image = new Image();
image.setUrl(album.getThumbnailURL());
image.setStyleName("album-thumbnail");
Hyperlink imageLink = new Hyperlink();
imageLink.getElement().getFirstChild().appendChild(image.getElement());
String target = getLocation(album.getName());
imageLink.setTargetHistoryToken(target);
FlowPanel mainPanel = new FlowPanel();
mainPanel.setStyleName("album-thumbnail-block");
mainPanel.addStyleName("inline-block");
mainPanel.add(imageLink);
Hyperlink titleLink = new Hyperlink(album.getTitle(), target);
titleLink.setStyleName("album-thumbnail-title");
mainPanel.add(titleLink);
thumbnails.add(mainPanel);
}
/**
* Adds a photo thumbnail to the panel.
*/
private void addPhotoThumbnail(Photo photo) {
Image image = new Image();
double scaleFactor = photo.getScaleFactor(128, 128);
image.setPixelSize((int)(photo.getWidth() * scaleFactor),
(int)(photo.getHeight() * scaleFactor));
image.setUrl(photo.getThumbnailURL());
image.setStyleName("photo-thumbnail");
Hyperlink imageLink = new Hyperlink();
imageLink.getElement().getFirstChild().appendChild(image.getElement());
String target = getLocation(albumName, photo.getID());
imageLink.setTargetHistoryToken(target);
imageLink.setStyleName("photo-thumbnail-block");
imageLink.addStyleName("inline-block");
thumbnails.add(imageLink);
}
private static interface LoadHandler {
void onLoaded();
}
private void loadAlbums(final LoadHandler handler) {
albums = null;
links = null;
// Request albums for the current user.
String url = "http://picasaweb.google.com/data/feed/api/" +
"user/" + (username.isEmpty() ? "default" : username) + "?alt=json";
PicasaWeb.request(url, new Callback<Albums>() {
public void callback(Albums response) {
albums = response;
if (links != null || !loadAlbumLinks) {
buildTrees();
handler.onLoaded();
}
}
});
if (!loadAlbumLinks) return;
// Load album links.
albumLinkService.getParentLinks(username,
new AsyncCallback<AlbumLink[]>() {
public void onSuccess(AlbumLink[] result) {
links = result;
if (albums != null) {
buildTrees();
handler.onLoaded();
}
}
public void onFailure(Throwable caught) {
setErrorMessage(caught.getMessage());
}
});
}
/**
* Loads the albums of the current user.
*/
private void loadAlbums() {
loadAlbums(new LoadHandler() {
public void onLoaded() {
setWindowTitle(null);
loadTree();
thumbnails.clear();
for (int i = 0; i < albums.getNumAlbums(); ++i)
addAlbumThumbnail(albums.getAlbum(i));
}
});
}
/**
* Loads the photos in the current album.
*/
private void loadPhotos(final LoadHandler handler) {
if (albums == null) {
loadAlbums(new LoadHandler() {
public void onLoaded() {
loadTree();
loadPhotos(handler);
}
});
return;
}
String url = null;
Album album = null;
for (int i = 0; i < albums.getNumAlbums(); ++i) {
if (albums.getAlbum(i).getName().equals(albumName)) {
album = albums.getAlbum(i);
break;
}
}
if (album == null) {
thumbnails.clear();
setErrorMessage("No such album: " + albumName);
return;
}
url = album.getURL();
setWindowTitle(album.getTitle());
PicasaWeb.request(url, new Callback<Photos>() {
public void callback(Photos response) {
thumbnails.clear();
photos = response;
for (int i = 0; i < response.getNumPhotos(); ++i)
addPhotoThumbnail(response.getPhoto(i));
handler.onLoaded();
}
});
}
private void addTreeItems(TreeItem item, List<AlbumTreeNode> children) {
for (AlbumTreeNode node: children) {
final Album album = node.getAlbum();
String target = getLocation(album.getName());
TreeItem child = item.addItem(
new Hyperlink(album.getTitle(), target));
addTreeItems(child, node.getChildren());
}
}
/**
* Loads the tree of albums of the current user.
*/
private void loadTree() {
if (tree == null) return;
tree.clear();
TreeItem root = tree.addItem(
new Hyperlink("Albums", getLocation(null)));
root.removeItems();
addTreeItems(root, roots);
root.setState(true);
}
/**
* Shows the photo with the specified ID.
* Requires photos to be loaded.
*/
private void showPhoto(String photoID) {
Photo photo = null;
for (int i = 0; i < photos.getNumPhotos(); ++i) {
final Photo p = photos.getPhoto(i);
if (p.getID().equals(photoID)) {
photoIndex = i;
photo = p;
break;
}
}
// Create a popup to show the full size image.
final PopupPanel imagePopup = new PopupPanel(true);
imagePopup.addCloseHandler(new CloseHandler<PopupPanel>() {
public void onClose(CloseEvent<PopupPanel> event) {
if (event.isAutoClosed())
hidePhoto();
}
});
// Scale to the size of the browser window's client area.
double scaleFactor = photo.getScaleFactor(Window.getClientWidth(),
Window.getClientHeight());
final int width = (int) (photo.getWidth() * scaleFactor);
Image image = new Image(photo.getURL(width));
FocusPanel focusPanel = new FocusPanel(image);
focusPanel.addKeyDownHandler(new KeyDownHandler() {
public void onKeyDown(KeyDownEvent event) {
switch (event.getNativeKeyCode()) {
case KeyCodes.KEY_RIGHT:
showNearbyPhoto(true);
break;
case KeyCodes.KEY_LEFT:
showNearbyPhoto(false);
break;
case KeyCodes.KEY_ESCAPE:
hidePhoto();
break;
}
}
});
imagePopup.setWidget(focusPanel);
image.setPixelSize(width, (int) (photo.getHeight() * scaleFactor));
final Element imageElement = image.getElement();
focusPanel.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
showNearbyPhoto(event.getRelativeX(imageElement) >= width / 2);
}
});
imagePopup.setGlassEnabled(true);
imagePopup.center();
focusPanel.setFocus(true);
}
/**
* Hides the currently displayed photo.
*/
private void hidePhoto() {
History.newItem(getLocation(albumName));
}
/**
* Shows the nearby (next or previous) photo.
*/
private void showNearbyPhoto(boolean next) {
if (next) {
if (photoIndex == photos.getNumPhotos() - 1)
return;
++photoIndex;
}
else {
if (photoIndex == 0)
return;
--photoIndex;
}
History.newItem(getLocation(albumName,
photos.getPhoto(photoIndex).getID()));
}
/**
* This is the entry point method.
*/
@Override
public void onModuleLoad() {
RootPanel root = RootPanel.get("tree");
if (root != null) {
tree = new Tree();
root.add(tree);
}
errorLabel = new Label();
RootPanel.get("error").add(errorLabel);
thumbnails = RootPanel.get("thumbnails");
Dictionary params = Dictionary.getDictionary("picasaapp_params");
if (params != null) {
Set<String> keys = params.keySet();
if (keys.contains("username")) {
username = params.get("username");
}
if (keys.contains("set_window_title")) {
setWindowTitle = Boolean.parseBoolean(
params.get("set_window_title"));
}
}
History.addValueChangeHandler(this);
History.fireCurrentHistoryState();
}
/**
* Changes the locations according to the event value.
*/
@Override
public void onValueChange(ValueChangeEvent<String> event) {
setErrorMessage("");
// The location should be in the form
// [username[/album-name[/photo-id]]],
// where [] denotes optional components.
String location = event.getValue();
String newUsername;
String newAlbumName;
if (showUsername) {
int slashPos = location.indexOf('/');
if (slashPos == -1) {
username = location;
albumName = null;
loadAlbums();
return;
}
newUsername = location.substring(0, slashPos);
newAlbumName = location.substring(slashPos + 1);
}
else {
if (location.isEmpty()) {
albumName = null;
loadAlbums();
return;
}
newUsername = username;
newAlbumName = location;
}
int slashPos = newAlbumName.indexOf('/');
String photoID = null;
if (slashPos != -1) {
photoID = newAlbumName.substring(slashPos + 1);
newAlbumName = newAlbumName.substring(0, slashPos);
}
if (!newUsername.equals(username) || !newAlbumName.equals(albumName)) {
username = newUsername;
albumName = newAlbumName;
final String finalPhotoID = photoID;
loadPhotos(new LoadHandler() {
public void onLoaded() {
if (finalPhotoID != null)
showPhoto(finalPhotoID);
}
});
}
else if (photoID != null)
showPhoto(photoID);
}
}