// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.client.explorer.youngandroid;
import java.util.Date;
import java.util.List;
import com.google.appinventor.client.ErrorReporter;
import com.google.appinventor.client.GalleryClient;
import com.google.appinventor.client.GalleryGuiFactory;
import com.google.appinventor.client.GalleryRequestListener;
import com.google.appinventor.client.Ode;
import com.google.appinventor.client.OdeAsyncCallback;
import com.google.appinventor.client.OdeMessages;
import com.google.appinventor.client.explorer.project.Project;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.utils.Uploader;
import com.google.appinventor.client.wizards.youngandroid.RemixedYoungAndroidProjectWizard;
import com.google.appinventor.shared.rpc.ServerLayout;
import com.google.appinventor.shared.rpc.UploadResponse;
import com.google.appinventor.shared.rpc.project.GalleryApp;
import com.google.appinventor.shared.rpc.project.GalleryAppListResult;
import com.google.appinventor.shared.rpc.project.GalleryComment;
import com.google.appinventor.shared.rpc.project.UserProject;
import com.google.appinventor.shared.rpc.user.User;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.ErrorEvent;
import com.google.gwt.event.dom.client.ErrorHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FileUpload;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.TabPanel;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
/**
* The gallery page shows a single app from the gallery
*
* It has different modes for public viewing or when user is publishing for first time
* or updating a previously published app
*
* @author wolberd@gmail.com (Dave Wolber)
* @author vincentaths@gmail.com (Vincent Zhang)
*/
public class GalleryPage extends Composite implements GalleryRequestListener {
public static final OdeMessages MESSAGES = GWT.create(OdeMessages.class);
final Ode ode = Ode.getInstance();
GalleryClient gallery = null;
GalleryGuiFactory galleryGF = new GalleryGuiFactory();
GalleryApp app = null;
String projectName = null;
Project project;
private final String HOLLOW_HEART_ICON_URL = "/images/numLikeHollow.png";
private final String RED_HEART_ICON_URL = "/images/numLike.png";
private final String DOWNLOAD_ICON_URL = "/images/numDownload.png";
private final String NUM_VIEW_ICON_URL = "/images/numView.png";
private final String NUM_COMMENT_ICON_URL = "/images/numComment.png";
private boolean imageUploaded = false;
private VerticalPanel panel; // the main panel
private FlowPanel galleryGUI;
private FlowPanel appSingle;
private FlowPanel appsByAuthor;
private FlowPanel appsByTags;
private FlowPanel appsRemixes;
private FlowPanel appDetails;
private FlowPanel appHeader;
private FlowPanel appInfo;
private FlowPanel appAction;
private FlowPanel appAuthor;
private FlowPanel appMeta;
private FlowPanel appDates;
private FlowPanel appPrimaryWrapper;
private FlowPanel appSecondaryWrapper;
private TabPanel appActionTabs;
private TabPanel sidebarTabs;
private FlowPanel appDescPanel;
private FlowPanel appReportPanel;
private FlowPanel appSharePanel;
private FlowPanel appComments;
private FlowPanel appCommentsList;
private FlowPanel returnToGallery;
//private String tagSelected;
public static final int VIEWAPP = 0;
public static final int NEWAPP = 1;
public static final int UPDATEAPP = 2;
private int editStatus;
private static final int MIN_DESC_LENGTH = 40;
/* Publish & edit state components */
private FlowPanel imageUploadBox;
private Label imageUploadPrompt;
private Image image;
private FileUpload upload;
private FlowPanel imageUploadBoxInner;
private FocusPanel wrapper;
private Label appCreated;
private Label appChanged;
private TextArea titleText;
private TextArea desc;
private TextArea moreInfoText;
private TextArea creditText;
private FlowPanel descBox;
private FlowPanel titleBox;
private Label likeCount;
private Button actionButton;
private Button removeButton;
private Button editButton;
private Button cancelButton;
private HTML ccLicenseRef;
/* Here is the organization of this page:
panel
galleryGUI
appSingle
appDetails
appClear
appHeader
wrapper (focus panel)
imageUploadBox (flow)
imageUploadBoxInner (flow)
imageUploadPRompt (label)
upload
image (this is put in dynamically)
appAction (button)
appInfo
title or titlebox
devName
appMeta
appDates
desc/descbox
appComments
appsByDev
divider
*/
/**
* Creates a new GalleryPage, must take in parameters
* @param app GalleryApp
* @param editStatus edit status
*/
public GalleryPage(final GalleryApp app, final int editStatus) {
// Get a reference to the Gallery Client which handles the communication to
// server to get gallery data
gallery = GalleryClient.getInstance();
gallery.addListener(this);
// We are either publishing a new app, updating, or just reading. If we are publishing
// a new app, app has some partial info to be published. Otherwise, it has all
// the info for the already published app
this.app = app;
this.editStatus = editStatus;
initComponents();
// App header - image
appHeader.addStyleName("app-header");
// If we're editing or updating, add input form for image
if (newOrUpdateApp()) {
initImageComponents();
} else { // we are just viewing this page so setup the image
initReadOnlyImage();
}
// Now let's add the button for publishing, updating, or trying
appHeader.add(appAction);
initActionButton();
if (editStatus==NEWAPP) {
initCancelButton();
/* Add Creative Commons Publishing Reference */
appAction.add(ccLicenseRef);
}
if (editStatus==UPDATEAPP) {
initRemoveButton();
initCancelButton();
/* Add Creative Commons Updating Reference */
appAction.add(ccLicenseRef);
}
// App details - app title
appInfo.add(titleBox);
initAppTitle(titleBox);
// App details - app author info
appInfo.add(appAuthor);
initAppAuthor(appAuthor);
// Not showing in new app becaus it doesn't have these info
// App details - meta
if (!newOrUpdateApp()) {
appInfo.add(appMeta);
initAppStats(appMeta);
}
// App details - dates
appInfo.add(appDates);
initAppMeta(appDates);
// App details - app description
appInfo.add(descBox);
initAppDesc(descBox, appDescPanel);
/**
* TODO: I may need to change the code logic here. appDescPanel is actually
* not added to [appInfo], instead in public state appDescPanel will be
* added into the [appActionTabs] (not showing in editable states) as a sub
* tab. So it may not be the best idea to modify appDescPanel in a method
* that resides in [appInfo]'s code block. - Vincent, 03/28/2014
*/
// Pass app components to App Detail container
appInfo.addStyleName("app-info-container");
appPrimaryWrapper.add(appHeader);
appPrimaryWrapper.add(appInfo);
appPrimaryWrapper.addStyleName("clearfix");
appDetails.add(appPrimaryWrapper);
// If app is in its public state, add action tabs
if (!newOrUpdateApp()) {
// Add a divider
HTML dividerPrimary = new HTML("<div class='section-divider'></div>");
appDetails.add(dividerPrimary);
// Initialize action tabs
initActionTabs();
// Initialize app share
initAppShare();
// Initialize app action features
initReportSection();
// We are not showing comments at initial launch, Such sadness :'[
/*
HTML dividerSecondary = new HTML("<div class='section-divider'></div>");
appDetails.add(dividerSecondary);
initAppComments();
*/
// Add sidebar stuff, only in public state
// By default, load the first tag's apps
gallery.GetAppsByDeveloper(0, 5, app.getDeveloperId());
}
// Add to appSingle
appSingle.add(appDetails);
appDetails.addStyleName("gallery-container");
appDetails.addStyleName("gallery-app-details");
if (!newOrUpdateApp()) {
appSingle.add(sidebarTabs);
sidebarTabs.addStyleName("gallery-container");
sidebarTabs.addStyleName("gallery-app-showcase");
}
// Add everything to top-level containers
galleryGUI.add(appSingle);
appSingle.addStyleName("gallery-app-single");
panel.add(galleryGUI);
galleryGUI.addStyleName("gallery");
initWidget(panel);
}
/**
* Helper method called by constructor to initialize ui components
*/
private void initComponents() {
// Initialize UI
panel = new VerticalPanel();
panel.setWidth("100%");
galleryGUI = new FlowPanel();
appSingle = new FlowPanel();
appDetails = new FlowPanel();
appHeader = new FlowPanel();
appInfo = new FlowPanel();
appAction = new FlowPanel();
appAuthor = new FlowPanel();
appMeta = new FlowPanel();
appDates = new FlowPanel();
appPrimaryWrapper = new FlowPanel();
appSecondaryWrapper = new FlowPanel();
appDescPanel = new FlowPanel();
appReportPanel = new FlowPanel();
appSharePanel = new FlowPanel();
appActionTabs = new TabPanel();
sidebarTabs = new TabPanel();
appComments = new FlowPanel();
appCommentsList = new FlowPanel();
appsByAuthor = new FlowPanel();
appsByTags = new FlowPanel();
appsRemixes = new FlowPanel();
returnToGallery = new FlowPanel();
// tagSelected = "";
appCreated = new Label();
appChanged = new Label();
descBox = new FlowPanel();
titleBox = new FlowPanel();
desc = new TextArea();
titleText = new TextArea();
moreInfoText = new TextArea();
creditText = new TextArea();
ccLicenseRef = new HTML(MESSAGES.galleryCcLicenseRef());
ccLicenseRef.addStyleName("app-action-html");
}
/**
* Helper method to check if the app is in its "editable" state
* If the app is in its "public" state it should return false
*/
private boolean newOrUpdateApp() {
if ((editStatus==NEWAPP) || (editStatus==UPDATEAPP))
return true;
else
return false;
}
/**
* Helper method called by constructor to initialize image upload components
*/
private void initImageComponents() {
imageUploadBox = new FlowPanel();
imageUploadBox.addStyleName("app-image-uploadbox");
imageUploadBox.addStyleName("gallery-editbox");
imageUploadBoxInner = new FlowPanel();
imageUploadPrompt = new Label("Upload your project image!");
imageUploadPrompt.addStyleName("gallery-editprompt");
updateAppImage(gallery.getCloudImageURL(app.getGalleryAppId()), imageUploadBoxInner);
image.addStyleName("status-updating");
imageUploadPrompt.addStyleName("app-image-uploadprompt");
imageUploadBoxInner.add(imageUploadPrompt);
upload = new FileUpload();
upload.addStyleName("app-image-upload");
upload.getElement().setAttribute("accept", "image/*");
// Set the correct handler for servlet side capture
upload.setName(ServerLayout.UPLOAD_FILE_FORM_ELEMENT);
upload.addChangeHandler(new ChangeHandler (){
public void onChange(ChangeEvent event) {
uploadImage();
}
});
imageUploadBoxInner.add(upload);
imageUploadBox.add(imageUploadBoxInner);
wrapper = new FocusPanel();
wrapper.add(imageUploadBox);
wrapper.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
// The correct way to trigger click event on FileUpload
upload.getElement().<InputElement>cast().click();
}
});
appHeader.add(wrapper);
}
/**
* Helper method called by constructor to create the app image for display
*/
private void initReadOnlyImage() {
updateAppImage(gallery.getCloudImageURL(app.getGalleryAppId()), appHeader);
}
/**
* Main method to validify and upload the app image
*/
private void uploadImage() {
String uploadFilename = upload.getFilename();
if (!uploadFilename.isEmpty()) {
// Grab and validify the filename
final String filename = makeValidFilename(uploadFilename);
// Forge the request URL for gallery servlet
// we used to send the gallery id to the servlet, now the project id as
// the servlet just stores image temporarily before publish
/* String uploadUrl = GWT.getModuleBaseURL() + ServerLayout.GALLERY_SERVLET +
"/apps/" + String.valueOf(app.getGalleryAppId()) + "/"+ filename; */
// send the project id as the id, to store image temporarily until published
String uploadUrl = GWT.getModuleBaseURL() + ServerLayout.GALLERY_SERVLET +
"/apps/" + String.valueOf(app.getProjectId()) + "/"+ filename;
Uploader.getInstance().upload(upload, uploadUrl,
new OdeAsyncCallback<UploadResponse>(MESSAGES.fileUploadError()) {
@Override
public void onSuccess(UploadResponse uploadResponse) {
switch (uploadResponse.getStatus()) {
case SUCCESS:
// Update the app image preview after a success upload
imageUploadBoxInner.clear();
// updateAppImage(app.getCloudImageURL(), imageUploadBoxInner);
updateAppImage(gallery.getProjectImageURL(app.getProjectId()),imageUploadBoxInner);
imageUploaded=true;
ErrorReporter.hide();
break;
case FILE_TOO_LARGE:
// The user can resolve the problem by uploading a smaller file.
ErrorReporter.reportInfo(MESSAGES.fileTooLargeError());
break;
default:
ErrorReporter.reportError(MESSAGES.fileUploadError());
break;
}
}
});
} else {
if (editStatus == NEWAPP) {
Window.alert(MESSAGES.noFileSelected());
}
}
}
/**
* Helper method to validify file name, used in uploadImage()
* @param uploadFilename The full filename of the file
*/
private String makeValidFilename(String uploadFilename) {
// Strip leading path off filename.
// We need to support both Unix ('/') and Windows ('\\') separators.
String filename = uploadFilename.substring(
Math.max(uploadFilename.lastIndexOf('/'), uploadFilename.lastIndexOf('\\')) + 1);
// We need to strip out whitespace from the filename.
filename = filename.replaceAll("\\s", "");
return filename;
}
/**
* Helper method to update the app image
* @param url The URL of the image to show
* @param container The container that image widget resides
*/
private void updateAppImage(String url, final Panel container) {
image = new Image();
image.addStyleName("app-image");
image.setUrl(url);
// if the user has provided a gallery app image, we'll load it. But if not
// the error will occur and we'll load default image
image.addErrorHandler(new ErrorHandler() {
public void onError(ErrorEvent event) {
image.setUrl(GalleryApp.DEFAULTGALLERYIMAGE);
}
});
container.add(image);
if(gallery.getSystemEnvironment() != null &&
gallery.getSystemEnvironment().toString().equals("Development")){
final OdeAsyncCallback<String> callback = new OdeAsyncCallback<String>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(String newUrl) {
image.setUrl(newUrl + "?" + System.currentTimeMillis());
}
};
Ode.getInstance().getGalleryService().getBlobServingUrl(url, callback);
}
}
/**
* Helper method called by constructor to create the app's main action button
*/
private void initActionButton () {
if (editStatus==NEWAPP)
initPublishButton();
else if (editStatus == UPDATEAPP)
initUpdateButton();
else{ // Public view state
initTryitButton();
initEdititButton();
}
}
/**
* Helper method called by constructor to initialize the app's title section
* @param container The container that title resides
*/
private void initAppTitle(Panel container) {
if (newOrUpdateApp()) {
// GUI for editable title container
if (editStatus==NEWAPP) {
// If it's new app, give a textual hint telling user this is title
titleText.setText(app.getTitle());
} else if (editStatus==UPDATEAPP) {
// If it's not new, just set whatever's in the data field already
titleText.setText(app.getTitle());
}
titleText.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
app.setTitle(titleText.getText());
}
});
titleText.addStyleName("app-desc-textarea");
container.add(titleText);
container.addStyleName("app-title-container");
} else {
Label title = new Label(app.getTitle());
title.addStyleName("app-title");
container.add(title);
}
}
/**
* Helper method called by constructor to initialize the author's info
* @param container The container that author's info resides
*/
private void initAppAuthor(Panel container) {
// Add author's image - not when creating a new app
if (editStatus != NEWAPP) {
final Image authorAvatar = new Image();
authorAvatar.addStyleName("app-userimage");
authorAvatar.setUrl(gallery.getUserImageURL(app.getDeveloperId()));
// If the user has provided a gallery app image, we'll load it. But if not
// the error will occur and we'll load default image
authorAvatar.addErrorHandler(new ErrorHandler() {
public void onError(ErrorEvent event) {
authorAvatar.setUrl(GalleryApp.DEFAULTUSERIMAGE);
}
});
authorAvatar.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
Ode.getInstance().switchToUserProfileView(
app.getDeveloperId(), 1 /* 1 for public view */ );
}
});
appInfo.add(authorAvatar);
}
// Add author's name
final Label authorName = new Label();
if (editStatus == NEWAPP) {
// App doesn't have author info yet, grab current user info
final User currentUser = Ode.getInstance().getUser();
authorName.setText(currentUser.getUserName());
} else {
authorName.setText(app.getDeveloperName());
authorName.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
Ode.getInstance().switchToUserProfileView(
app.getDeveloperId(), 1 /* 1 for public view*/ );
}
});
}
authorName.addStyleName("app-username");
authorName.addStyleName("app-subtitle");
appInfo.add(authorName);
}
/**
* Helper method called by constructor to initialize the app's stats fields
* @param container The container that stats fields reside
*/
private void initAppStats(Panel container) {
// Images for stats data
Image numDownloads = new Image();
numDownloads.setUrl(DOWNLOAD_ICON_URL);
Image numLikes = new Image();
numLikes.setUrl(HOLLOW_HEART_ICON_URL);
// Add stats data
container.addStyleName("app-stats");
container.add(numDownloads);
container.add(new Label(Integer.toString(app.getDownloads())));
// Adds dynamic like
initLikeSection(container);
// Adds dynamic feature
initFeatureSection(container);
// Adds dynamic tutorial
initTutorialSection(container);
// Adds dynamic salvage
initSalvageSection(container);
// We are not using views and comments at initial launch
/*
Image numViews = new Image();
numViews.setUrl(NUM_VIEW_ICON_URL);
Image numComments = new Image();
numComments.setUrl(NUM_COMMENT_ICON_URL);
container.add(numViews);
container.add(new Label(Integer.toString(app.getViews())));
container.add(numComments);
container.add(new Label(Integer.toString(app.getComments())));
*/
}
/**
* Helper method called by constructor to initialize the app's meta fields
* @param container The container that date fields reside
*/
private void initAppMeta(Panel container) {
Date createdDate = new Date();
Date changedDate = new Date();
if (editStatus == NEWAPP) {
} else {
createdDate = new Date(app.getCreationDate());
changedDate = new Date(app.getUpdateDate());
}
DateTimeFormat dateFormat = DateTimeFormat.getFormat("yyyy/MM/dd");
Label appCreatedLabel = new Label(MESSAGES.galleryCreatedDateLabel());
appCreatedLabel.addStyleName("app-meta-label");
container.add(appCreatedLabel);
appCreated.setText(dateFormat.format(createdDate));
container.add(appCreated);
Label appChangedLabel = new Label(MESSAGES.galleryChangedDateLabel());
appChangedLabel.addStyleName("app-meta-label");
container.add(appChangedLabel);
appChanged.setText(dateFormat.format(changedDate));
container.add(appChanged);
if (newOrUpdateApp()) {
// GUI for editable title container
// Set the placeholders of textarea
moreInfoText.getElement().setPropertyString("placeholder", MESSAGES.galleryMoreInfoHint());
creditText.getElement().setPropertyString("placeholder", MESSAGES.galleryCreditHint());
if (editStatus==NEWAPP) {
// If it's a new app, it will show the placeholder hint
} else if (editStatus==UPDATEAPP) {
// If it's not new, just set whatever's in the data field already
moreInfoText.setText(app.getMoreInfo());
creditText.setText(app.getCredit());
}
moreInfoText.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
app.setMoreInfo(moreInfoText.getText());
}
});
creditText.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
app.setCredit(creditText.getText());
}
});
moreInfoText.addStyleName("app-desc-textarea");
creditText.addStyleName("app-desc-textarea");
container.add(moreInfoText);
container.add(creditText);
} else { // Public app view
String linktext = makeValidLink(app.getMoreInfo());
if(linktext != null){
Label moreInfoLabel = new Label(MESSAGES.galleryMoreInfoLabel());
moreInfoLabel.addStyleName("app-meta-label");
container.add(moreInfoLabel);
Anchor userLinkDisplay = new Anchor();
userLinkDisplay.setText(linktext);
userLinkDisplay.setHref(linktext);
userLinkDisplay.setTarget("_blank");
container.add(userLinkDisplay);
}
//"remixed from" field
container.add(initRemixFromButton());
//"credits" field
if(app.getCredit() != null && app.getCredit().length() > 0){
Label creditLabel = new Label(MESSAGES.galleryCreditLabel());
creditLabel.addStyleName("app-meta-label");
container.add(creditLabel);
Label creditText = new Label(app.getCredit());
container.add(creditText);
}
}
container.addStyleName("app-meta");
}
/**
* Helper method to validify a hyperlink
* @param linktext the actual http link that the anchor should point to
* @return linktext a valid http link or null.
*/
private String makeValidLink(String linktext) {
if (linktext == null) {
return null;
} else {
if (linktext.isEmpty()) {
return null;
} else {
// Validate link format, fill in http part
if (!linktext.toLowerCase().startsWith("http")) {
linktext = "http://" + linktext;
}
return linktext;
}
}
}
/**
* Helper method called by constructor to initialize the app's description
* @param c1 The container that description resides (editable state)
* @param c2 The container that description resides (public state)
*/
private void initAppDesc(Panel c1, Panel c2) {
desc.getElement().setPropertyString("placeholder", MESSAGES.galleryDescriptionHint());
if (newOrUpdateApp()) {
desc.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
app.setDescription(desc.getText());
}
});
if(editStatus==UPDATEAPP){
desc.setText(app.getDescription());
}
desc.addStyleName("app-desc-textarea");
c1.add(desc);
} else {
Label description = new Label(app.getDescription());
c2.add(description);
c2.addStyleName("app-description");
}
}
/**
* Helper method called by constructor to initialize the app's comment area
*/
private void initAppComments() {
// App details - comments
appDetails.add(appComments);
appComments.addStyleName("app-comments-wrapper");
Label commentsHeader = new Label("Comments and Reviews");
commentsHeader.addStyleName("app-comments-header");
appComments.add(commentsHeader);
final TextArea commentTextArea = new TextArea();
commentTextArea.addStyleName("app-comments-textarea");
appComments.add(commentTextArea);
Button commentSubmit = new Button("Submit my comment");
commentSubmit.addStyleName("app-comments-submit");
commentSubmit.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
final OdeAsyncCallback<Long> commentPublishCallback = new OdeAsyncCallback<Long>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Long date) {
// get the new comment list so gui updates
// note: we might modify the call to publishComment so it returns
// the list instead, this would save one server call
gallery.GetComments(app.getGalleryAppId(), 0, 100);
}
};
Ode.getInstance().getGalleryService().publishComment(app.getGalleryAppId(),
commentTextArea.getText(), commentPublishCallback);
}
});
appComments.add(commentSubmit);
// Add list of comments
gallery.GetComments(app.getGalleryAppId(), 0, 100);
appComments.add(appCommentsList);
appCommentsList.addStyleName("app-comments");
}
/**
* Helper method called by constructor to initialize the app action tabs
*/
private void initActionTabs() {
// Add a bunch of tabs for executable actions regarding the app
appSecondaryWrapper.addStyleName("clearfix");
appSecondaryWrapper.add(appActionTabs);
appActionTabs.addStyleName("app-actions");
appActionTabs.add(appDescPanel, "Description");
appActionTabs.add(appSharePanel, "Share");
appActionTabs.add(appReportPanel, "Report");
appActionTabs.selectTab(0);
appActionTabs.addStyleName("app-actions-tabs");
appDetails.add(appSecondaryWrapper);
// Return to Gallery link
Label returnLabel = new Label("Back to Gallery");
returnLabel.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent clickEvent) {
ode.switchToGalleryView();
}
});
returnToGallery.add(returnLabel);
returnToGallery.addStyleName("gallery-nav-return");
returnToGallery.addStyleName("primary-link");
appSecondaryWrapper.add(returnToGallery); //
}
/**
* Helper method called by constructor to initialize the remix button
*/
private FlowPanel initRemixFromButton(){
FlowPanel container = new FlowPanel();
final Label remixedFrom = new Label(MESSAGES.galleryRemixedFrom());
remixedFrom.addStyleName("app-meta-label");
final Label parentApp = new Label();
//gwt-Label use fixed width which will case border-underline-dot
//be longer than text link.
//gwt-Label-auto use auto width
parentApp.removeStyleName("gwt-Label");
parentApp.addStyleName("gwt-Label-auto");
parentApp.addStyleName("primary-link");
container.add(remixedFrom);
container.add(parentApp);
remixedFrom.setVisible(false);
parentApp.setVisible(false);
final Result<GalleryApp> attributionGalleryApp = new Result<GalleryApp>();
final OdeAsyncCallback<Long> remixedFromCallback = new OdeAsyncCallback<Long>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(final Long attributionId) {
if (attributionId != UserProject.FROMSCRATCH) {
remixedFrom.setVisible(true);
parentApp.setVisible(true);
final OdeAsyncCallback<GalleryApp> callback = new OdeAsyncCallback<GalleryApp>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(GalleryApp AppRemixedFrom) {
parentApp.setText(AppRemixedFrom.getTitle());
attributionGalleryApp.t = AppRemixedFrom;
}
};
Ode.getInstance().getGalleryService().getApp(attributionId, callback);
} else {
attributionGalleryApp.t = null;
}
}
};
Ode.getInstance().getGalleryService().remixedFrom(app.getGalleryAppId(), remixedFromCallback);
parentApp.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if (attributionGalleryApp.t == null) {
} else {
Ode.getInstance().switchToGalleryAppView(attributionGalleryApp.t, GalleryPage.VIEWAPP);
}
}
});
final OdeAsyncCallback<List<GalleryApp>> callback = new OdeAsyncCallback<List<GalleryApp>>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(final List<GalleryApp> apps) {
if (apps.size() != 0) {
// Display remixes at the sidebar on the same page
galleryGF.generateSidebar(apps, sidebarTabs, appsRemixes, "Remixes",
MESSAGES.galleryAppsRemixesSidebar() + app.getTitle(), false, false);
}
}
};
Ode.getInstance().getGalleryService().remixedTo(app.getGalleryAppId(), callback);
return container;
}
/**
* Helper method called by constructor to initialize the report section
*/
private void initReportSection() {
final HTML reportPrompt = new HTML();
reportPrompt.setHTML(MESSAGES.galleryReportPrompt());
reportPrompt.addStyleName("primary-prompt");
final TextArea reportText = new TextArea();
reportText.addStyleName("action-textarea");
final Button submitReport = new Button(MESSAGES.galleryReportButton());
submitReport.addStyleName("action-button");
final Label descriptionError = new Label();
descriptionError.setText("Description required");
descriptionError.setStyleName("ode-ErrorMessage");
descriptionError.setVisible(false);
appReportPanel.add(reportPrompt);
appReportPanel.add(descriptionError);
appReportPanel.add(reportText);
appReportPanel.add(submitReport);
final OdeAsyncCallback<Boolean> isReportdByUserCallback = new OdeAsyncCallback<Boolean>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean isAlreadyReported) {
if(isAlreadyReported) { //already reported, cannot report again
reportPrompt.setHTML(MESSAGES.galleryAlreadyReportedPrompt());
reportText.setVisible(false);
submitReport.setVisible(false);
submitReport.setEnabled(false);
} else {
submitReport.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
final OdeAsyncCallback<Long> reportClickCallback = new OdeAsyncCallback<Long>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Long id) {
reportPrompt.setHTML(MESSAGES.galleryReportCompletionPrompt());
reportText.setVisible(false);
submitReport.setVisible(false);
submitReport.setEnabled(false);
}
};
if (!reportText.getText().trim().isEmpty()){
Ode.getInstance().getGalleryService().addAppReport(app, reportText.getText(),
reportClickCallback);
descriptionError.setVisible(false);
} else {
descriptionError.setVisible(true);
}
}
});
}
}
};
Ode.getInstance().getGalleryService().isReportedByUser(app.getGalleryAppId(),
isReportdByUserCallback);
}
/**
* Helper method called by constructor to initialize the report section
*/
private void initAppShare() {
final HTML sharePrompt = new HTML();
sharePrompt.setHTML(MESSAGES.gallerySharePrompt());
sharePrompt.addStyleName("primary-prompt");
final TextBox urlText = new TextBox();
urlText.addStyleName("action-textbox");
urlText.setText(Window.Location.getHost() + MESSAGES.galleryGalleryIdAction() + app.getGalleryAppId());
urlText.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
urlText.selectAll();
}
});
appSharePanel.add(sharePrompt);
appSharePanel.add(urlText);
}
/**
* Helper method called by constructor to initialize the like section
* @param container The container that like label & image reside
*/
private void initLikeSection(Panel container) { //TODO: Update the location of this button
final Image likeButton = new Image();
likeButton.setUrl(HOLLOW_HEART_ICON_URL);
container.add(likeButton);
likeCount = new Label(MESSAGES.galleryEmptyText());
container.add(likeCount);
final Label likePrompt = new Label(MESSAGES.galleryEmptyText());
likePrompt.addStyleName("primary-link");
container.add(likePrompt);
likePrompt.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
final OdeAsyncCallback<Integer> changeLikeCallback = new OdeAsyncCallback<Integer>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Integer num) {
// TODO: deal with/discuss server data sync later; now is updating locally.
final OdeAsyncCallback<Boolean> checkCallback = new OdeAsyncCallback<Boolean>(
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean b) {
//email will be send automatically if condition matches (in ObjectifyGalleryStorageIo)
}
};
Ode.getInstance().getGalleryService().checkIfSendAppStats(app.getDeveloperId(), app.getGalleryAppId(),
gallery.getGallerySettings().getAdminEmail(), Window.Location.getHost(), checkCallback);
}
};
final OdeAsyncCallback<Boolean> isLikedByUserCallback = new OdeAsyncCallback<Boolean>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean bool) {
if (bool) { // If the app is already liked before, and user clicks again, that means unlike
Ode.getInstance().getGalleryService().decreaseLikes(app.getGalleryAppId(),
changeLikeCallback);
likePrompt.setText(MESSAGES.galleryAppsLike());
// Old code
likeCount.setText(String.valueOf(Integer.valueOf(likeCount.getText()) - 1));
likeButton.setUrl(HOLLOW_HEART_ICON_URL); // Unliked
} else {
// If the app is not yet liked, and user clicks like, that means add a like
Ode.getInstance().getGalleryService().increaseLikes(app.getGalleryAppId(),
changeLikeCallback);
likePrompt.setText(MESSAGES.galleryAppsAlreadyLike());
// Old code
likeCount.setText(String.valueOf(Integer.valueOf(likeCount.getText()) + 1));
likeButton.setUrl(RED_HEART_ICON_URL); // Liked
}
}
};
Ode.getInstance().getGalleryService().isLikedByUser(app.getGalleryAppId(),
isLikedByUserCallback); // This happens when user click on like, we need to check if it's already liked
}
});
final OdeAsyncCallback<Integer> likeNumCallback = new OdeAsyncCallback<Integer>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Integer num) {
likeCount.setText(String.valueOf(num));
}
};
Ode.getInstance().getGalleryService().getNumLikes(app.getGalleryAppId(),
likeNumCallback);
final OdeAsyncCallback<Boolean> isLikedCallback = new OdeAsyncCallback<Boolean>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean bool) {
if (!bool) {
likePrompt.setText(MESSAGES.galleryAppsLike());
likeButton.setUrl(HOLLOW_HEART_ICON_URL);//unliked
} else {
likePrompt.setText(MESSAGES.galleryAppsAlreadyLike());
likeButton.setUrl(RED_HEART_ICON_URL);//liked
}
}
};
Ode.getInstance().getGalleryService().isLikedByUser(app.getGalleryAppId(),
isLikedCallback);
}
/**
* Helper method called by constructor to initialize the salvage section
* @param container The container that salvage label reside
*/
private void initSalvageSection(Panel container) { //TODO: Update the location of this button
if (!canSalvage()) { // Permitted to salvage?
return;
}
final Label salvagePrompt = new Label("salvage");
salvagePrompt.addStyleName("primary-link");
container.add(salvagePrompt);
salvagePrompt.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
final OdeAsyncCallback<Void> callback = new OdeAsyncCallback<Void>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Void bool) {
salvagePrompt.setText("done");
}
};
Ode.getInstance().getGalleryService().salvageGalleryApp(app.getGalleryAppId(), callback);
}
});
}
/**
* Helper method called by constructor to initialize the feature section
* @param container The container that feature label reside
*/
private void initFeatureSection(Panel container) { //TODO: Update the location of this button
final User currentUser = Ode.getInstance().getUser();
if(currentUser.getType() != User.MODERATOR){ //not admin
return;
}
final Label featurePrompt = new Label(MESSAGES.galleryEmptyText());
featurePrompt.addStyleName("primary-link");
container.add(featurePrompt);
final OdeAsyncCallback<Boolean> isFeaturedCallback = new OdeAsyncCallback<Boolean>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean bool) {
if (bool) { // If the app is already featured before, the prompt should show as unfeatured
featurePrompt.setText(MESSAGES.galleryUnfeaturedText());
} else { // otherwise show as featured
featurePrompt.setText(MESSAGES.galleryFeaturedText());
}
}
};
Ode.getInstance().getGalleryService().isFeatured(app.getGalleryAppId(),
isFeaturedCallback); // This happens when user click on like, we need to check if it's already liked
featurePrompt.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
final OdeAsyncCallback<Boolean> markFeaturedCallback = new OdeAsyncCallback<Boolean>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean bool) {
if (bool) { // If the app is already featured, the prompt should show as unfeatured
featurePrompt.setText(MESSAGES.galleryUnfeaturedText());
} else { // otherwise show as featured
featurePrompt.setText(MESSAGES.galleryFeaturedText());
}
//update gallery list
gallery.appWasChanged();
}
};
Ode.getInstance().getGalleryService().markAppAsFeatured(app.getGalleryAppId(),
markFeaturedCallback);
}
});
}
/**
* Helper method called by constructor to initialize the tutorial section
* @param container The container that feature label reside
*/
private void initTutorialSection(Panel container) { //TODO: Update the location of this button
final User currentUser = Ode.getInstance().getUser();
if(currentUser.getType() != User.MODERATOR){ //not admin
return;
}
final Label tutorialPrompt = new Label(MESSAGES.galleryEmptyText());
tutorialPrompt.addStyleName("primary-link");
container.add(tutorialPrompt);
final OdeAsyncCallback<Boolean> isTutorialCallback = new OdeAsyncCallback<Boolean>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean bool) {
if (bool) { // If the app is already featured before, the prompt should show as unfeatured
tutorialPrompt.setText(MESSAGES.galleryUntutorialText());
} else { // otherwise show as featured
tutorialPrompt.setText(MESSAGES.galleryTutorialText());
}
}
};
Ode.getInstance().getGalleryService().isTutorial(app.getGalleryAppId(),
isTutorialCallback); // This happens when user click on like, we need to check if it's already liked
tutorialPrompt.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
final OdeAsyncCallback<Boolean> markTutorialCallback = new OdeAsyncCallback<Boolean>(
// failure message
MESSAGES.galleryError()) {
@Override
public void onSuccess(Boolean bool) {
if (bool) { // If the app is already featured, the prompt should show as unfeatured
tutorialPrompt.setText(MESSAGES.galleryUntutorialText());
} else { // otherwise show as featured
tutorialPrompt.setText(MESSAGES.galleryTutorialText());
}
//update gallery list
gallery.appWasChanged();
}
};
Ode.getInstance().getGalleryService().markAppAsTutorial(app.getGalleryAppId(),
markTutorialCallback);
}
});
}
/**
* Helper method called by constructor to initialize the edit it button
* Only seen by app owner.
*/
private void initEdititButton() {
final User currentUser = Ode.getInstance().getUser();
if(app.getDeveloperId().equals(currentUser.getUserId())){
editButton = new Button(MESSAGES.galleryEditText());
editButton.addClickHandler(new ClickHandler() {
// Open up source file if clicked the action button
public void onClick(ClickEvent event) {
editButton.setEnabled(false);
Ode.getInstance().switchToGalleryAppView(app, GalleryPage.UPDATEAPP);
}
});
editButton.addStyleName("app-action-button");
appAction.add(editButton);
}
}
/**
* Helper method called by constructor to initialize the try it button
*/
private void initTryitButton() {
actionButton = new Button(MESSAGES.galleryOpenText());
actionButton.addClickHandler(new ClickHandler() {
// Open up source file if clicked the action button
public void onClick(ClickEvent event) {
actionButton.setEnabled(false);
/*
* open a popup window that will prompt to ask user to enter
* a new project name(if "new name" is not valid, user may need to
* enter again). After that, "loadSourceFil" and "appWasDownloaded"
* will be called.
*/
new RemixedYoungAndroidProjectWizard(app, actionButton).center();
}
});
actionButton.addStyleName("app-action-button");
appAction.add(actionButton);
}
/**
* Helper method called by constructor to initialize the publish button
*/
private void initPublishButton() {
actionButton = new Button(MESSAGES.galleryPublishText());
actionButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if(!checkIfReadyToPublishOrUpdateApp(app)){
return;
}
actionButton.setEnabled(false);
actionButton.setText(MESSAGES.galleryAppPublishing());
final OdeAsyncCallback<GalleryApp> callback = new OdeAsyncCallback<GalleryApp>(
MESSAGES.galleryError()) {
@Override
// When publish or update call returns
public void onSuccess(final GalleryApp gApp) {
// we only set the projectId to the gallery app if new app. If we
// are updating its already set
final OdeAsyncCallback<Void> projectCallback = new OdeAsyncCallback<Void>(
MESSAGES.galleryError()) {
@Override
public void onSuccess(Void result) {
// this is called after published and after we've set the gallery id
// tell the project list to change project's button to "Update"
Ode.getInstance().getProjectManager().publishProject(app.getProjectId(),
gApp.getGalleryAppId());
Ode.getInstance().switchToGalleryAppView(gApp, GalleryPage.VIEWAPP);
// above was app, switched to gApp which is the newly published thing
final OdeAsyncCallback<Long> attributionCallback = new OdeAsyncCallback<Long>(
MESSAGES.galleryError()) {
@Override
public void onSuccess(Long result) {
}
};
Ode.getInstance().getGalleryService().saveAttribution(gApp.getGalleryAppId(), app.getProjectAttributionId(),
attributionCallback);
}//end of projectCallback#onSuccess
@Override
public void onFailure(Throwable caught) {
super.onFailure(caught);
actionButton.setEnabled(true);
actionButton.setText(MESSAGES.galleryPublishText());
}
};//end of projectCallback
Ode.getInstance().getProjectService().setGalleryId(gApp.getProjectId(),
gApp.getGalleryAppId(), projectCallback);
// we need to update the app object for this gallery page
gallery.appWasChanged();
}//end of callback#onSuccess
@Override
public void onFailure(Throwable caught) {
Window.alert(MESSAGES.galleryNoExtensionsPlease());
actionButton.setEnabled(true);
actionButton.setText(MESSAGES.galleryPublishText());
}
};
// call publish with the default app data...
Ode.getInstance().getGalleryService().publishApp(app.getProjectId(),
app.getTitle(), app.getProjectName(), app.getDescription(), app.getMoreInfo(), app.getCredit(), callback);
}
});
actionButton.addStyleName("app-action-button");
appAction.add(actionButton);
}
/**
* Helper method called by constructor to initialize the publish button
*/
private void initUpdateButton() {
actionButton = new Button(MESSAGES.galleryUpdateText());
actionButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if(!checkIfReadyToPublishOrUpdateApp(app)){
return;
}
actionButton.setEnabled(false);
actionButton.setText(MESSAGES.galleryAppUpdating());
final OdeAsyncCallback<Void> updateSourceCallback = new OdeAsyncCallback<Void>(
MESSAGES.galleryError()) {
@Override
public void onSuccess(Void result) {
gallery.appWasChanged(); // to update the gallery list and page
Ode.getInstance().switchToGalleryAppView(app, GalleryPage.VIEWAPP);
}
@Override
public void onFailure(Throwable caught) {
Window.alert(MESSAGES.galleryNoExtensionsPlease());
actionButton.setEnabled(true);
actionButton.setText(MESSAGES.galleryUpdateText());
}
};
Ode.getInstance().getGalleryService().updateApp(app,imageUploaded,updateSourceCallback);
}
});
actionButton.addStyleName("app-action-button");
appAction.add(actionButton);
}
/**
* check if it is ready to publish or update GalleryApp
* 1.The minimum length of Desc must be at least MIN_DESC_LENGTH
* 2.User must upload an image first, in order to publish GaleryApp
* @param app
* @return
*/
private boolean checkIfReadyToPublishOrUpdateApp(GalleryApp app){
if(app.getDescription().length() < MIN_DESC_LENGTH){
Window.alert(MESSAGES.galleryNotEnoughDescriptionMessage());
return false;
}
if(!imageUploaded && editStatus==NEWAPP){
/*we only need to check the image on the publish status*/
Window.alert(MESSAGES.galleryNoScreenShotMessage());
return false;
}
return true;
}
/**
* Helper method called by constructor to initialize the remove button
*/
private void initRemoveButton() {
removeButton = new Button(MESSAGES.galleryRemoveText());
removeButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
//popup confrim dialog
if(!Window.confirm(MESSAGES.galleryRemoveConfirmText())) {
return;
}
removeButton.setEnabled(false);
removeButton.setText(MESSAGES.galleryAppRemoving());;
final OdeAsyncCallback<Void> callback = new OdeAsyncCallback<Void>(
MESSAGES.galleryDeleteError()) {
@Override
public void onSuccess(Void result) {
// once we have deleted, set the project id back to not published
final OdeAsyncCallback<Void> projectCallback = new OdeAsyncCallback<Void>(
MESSAGES.gallerySetProjectIdError()) {
@Override
public void onSuccess(Void result) {
// this is called after deleted and after we've set the galleryid
Ode.getInstance().getProjectManager().UnpublishProject(app.getProjectId());
Ode.getInstance().switchToProjectsView();
}
@Override
public void onFailure(Throwable caught) {
super.onFailure(caught);
removeButton.setEnabled(true);
removeButton.setText(MESSAGES.galleryRemoveText());
}
};
GalleryClient client = GalleryClient.getInstance();
client.appWasChanged(); // tell views to update
Ode.getInstance().getProjectService().setGalleryId(app.getProjectId(),
UserProject.NOTPUBLISHED, projectCallback);
}
@Override
public void onFailure(Throwable caught) {
super.onFailure(caught);
removeButton.setEnabled(true);
removeButton.setText(MESSAGES.galleryRemoveText());
}
};
Ode.getInstance().getGalleryService().deleteApp(app.getGalleryAppId(),callback);
}
});
removeButton.addStyleName("app-action-button");
appAction.add(removeButton);
}
/**
* Helper method called by constructor to initialize the cancel button
*/
private void initCancelButton() {
cancelButton = new Button(MESSAGES.galleryCancelText());
cancelButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
if (editStatus==NEWAPP) {
Ode.getInstance().switchToProjectsView();
}else if(editStatus==UPDATEAPP){
Ode.getInstance().switchToGalleryAppView(app, GalleryPage.VIEWAPP);
}
}
});
cancelButton.addStyleName("app-action-button");
appAction.add(cancelButton);
}
/**
* Loads the proper tab GUI with gallery's app data.
* @param appResults: list of returned gallery apps from callback.
* @param requestId: determines the specific type of app data.
*/
private void refreshApps(GalleryAppListResult appResults, int requestId, boolean refreshable) {
switch (requestId) {
case GalleryClient.REQUEST_BYDEVELOPER:
galleryGF.generateSidebar(appResults.getApps(), sidebarTabs, appsByAuthor, MESSAGES.galleryByAuthorText(), MESSAGES.galleryAppsByAuthorSidebar() + MESSAGES.gallerySingleSpaceText() + app.getDeveloperName(), true, true);
break;
// case GalleryClient.REQUEST_BYTAG: /* We are not implementing tags at initial launch */
// String tagTitle = "Tagged with " + tagSelected;
// galleryGF.generateSidebar(apps, appsByTags, tagTitle, true);
// break;
}
}
/**
* When the gallery client gets some apps it fires this callback for
* gallery page to listen to
*/
@Override
public boolean onAppListRequestCompleted(GalleryAppListResult appResults, int requestId, boolean refreshable) {
if (appResults != null && appResults.getApps() != null)
refreshApps(appResults, requestId, refreshable);
else
OdeLog.log("apps was null");
return false;
}
/**
* When the gallery client gets some comments it fires this callback for
* gallery page to listen to
*/
@Override
public boolean onCommentsRequestCompleted(List<GalleryComment> comments) {
galleryGF.generateAppPageComments(comments, appCommentsList);
if (comments == null)
OdeLog.log("comment list was null");
return false;
}
@Override
public boolean onSourceLoadCompleted(UserProject projectInfo) {
return false;
}
/**
* Routine to determine if this user can salvage likes on a Gallery App
* Verifies that they are a Gallery Moderator AND a site Admin.
*
* @return boolean true if permitted
*/
private boolean canSalvage() {
User currentUser = Ode.getInstance().getUser();
if ((currentUser.getType() == User.MODERATOR)
&& currentUser.getIsAdmin()) {
return true;
} else {
return false;
}
}
/**
* Create a final object of this class to hold a modifiable result value that
* can be used in a method of an inner class
*/
private class Result<T> {
T t;
}
/**
* Creates a new null GalleryPage.
* This is only used for init in GalleryAppBox.java, do not use this normally
*
*/
public GalleryPage() {
}
}