Commit fc2a232c authored by Aaron Suarez's avatar Aaron Suarez Committed by Evan W. Patton

Add media preview

Change-Id: I6702e49a89777c1aa65c6a9fb766e0980d0fc380
parent 2961d7df
...@@ -586,4 +586,22 @@ public interface Images extends Resources { ...@@ -586,4 +586,22 @@ public interface Images extends Resources {
@Source("com/google/appinventor/images/rectangle.png") @Source("com/google/appinventor/images/rectangle.png")
ImageResource rectangle(); ImageResource rectangle();
/**
* Media icon: image
*/
@Source("com/google/appinventor/images/mediaIcon_img.png")
ImageResource mediaIconImg();
/**
* Media icon: audio
*/
@Source("com/google/appinventor/images/mediaIcon_audio.png")
ImageResource mediaIconAudio();
/**
* Media icon: video
*/
@Source("com/google/appinventor/images/mediaIcon_video.png")
ImageResource mediaIconVideo();
} }
...@@ -1376,6 +1376,13 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages { ...@@ -1376,6 +1376,13 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages {
String errorGeneratingYail(String formName); String errorGeneratingYail(String formName);
// Used in explorer/commands/CommandRegistory.java // Used in explorer/commands/CommandRegistory.java
@DefaultMessage("Preview...")
@Description("Label for the context menu command that previews a file")
String previewFileCommand();
@DefaultMessage("Close Preview")
@Description("Text for closing a file preview window")
String closeFilePreview();
@DefaultMessage("Delete...") @DefaultMessage("Delete...")
@Description("Label for the context menu command that deletes a file") @Description("Label for the context menu command that deletes a file")
...@@ -1385,6 +1392,15 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages { ...@@ -1385,6 +1392,15 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages {
@Description("Label for the context menu command that downloads a file") @Description("Label for the context menu command that downloads a file")
String downloadFileCommand(); String downloadFileCommand();
// Used in explore/commands/FilePreviewCommand.java
@DefaultMessage("Unfortunately, a preview for this file is unavailable.")
@Description("Text for files not compatible with HTML5 elements")
String filePreviewError();
@DefaultMessage("Unfortunately, your browser does not support playback of this file.")
@Description("Text for browsers not compatable with HTML5 elements")
String filePlaybackError();
// Used in explorer/commands/CopyYoungAndroidProjectCommand.java // Used in explorer/commands/CopyYoungAndroidProjectCommand.java
@DefaultMessage("Checkpoint - {0}") @DefaultMessage("Checkpoint - {0}")
......
...@@ -64,6 +64,8 @@ public class CommandRegistry extends MultiRegistry<ProjectNode, CommandRegistry. ...@@ -64,6 +64,8 @@ public class CommandRegistry extends MultiRegistry<ProjectNode, CommandRegistry.
super(ProjectNode.class); super(ProjectNode.class);
// Files // Files
registerCommand(FileNode.class, new ProjectNodeCommand(MESSAGES.previewFileCommand(),
Tracking.PROJECT_ACTION_PREVIEW_FILE_YA, new PreviewFileCommand()));
registerCommand(FileNode.class, new ProjectNodeCommand(MESSAGES.deleteFileCommand(), registerCommand(FileNode.class, new ProjectNodeCommand(MESSAGES.deleteFileCommand(),
Tracking.PROJECT_ACTION_DELETE_FILE_YA, new DeleteFileCommand())); Tracking.PROJECT_ACTION_DELETE_FILE_YA, new DeleteFileCommand()));
registerCommand(FileNode.class, new ProjectNodeCommand(MESSAGES.downloadFileCommand(), registerCommand(FileNode.class, new ProjectNodeCommand(MESSAGES.downloadFileCommand(),
......
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2016 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.commands;
import com.google.appinventor.shared.storage.StorageUtil;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.appinventor.shared.rpc.project.ProjectNode;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import static com.google.appinventor.client.Ode.MESSAGES;
/**
* Command for previewing files.
*
*/
public class PreviewFileCommand extends ChainableCommand {
/**
* Creates a new command for previewing a file.
*/
public PreviewFileCommand() {
super(null); // no next command
}
@Override
public boolean willCallExecuteNextCommand() {
return false;
}
@Override
public void execute(final ProjectNode node) {
final DialogBox dialogBox = new DialogBox();
dialogBox.setText(node.getName());
dialogBox.setStylePrimaryName("ode-DialogBox");
//setting position of dialog box
dialogBox.center();
dialogBox.setAnimationEnabled(true);
//button element
final Button closeButton = new Button(MESSAGES.closeFilePreview());
closeButton.getElement().setId("closeButton");
closeButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
dialogBox.hide();
}
});
HorizontalPanel buttonPanel = new HorizontalPanel();
buttonPanel.setHorizontalAlignment(HorizontalPanel.ALIGN_CENTER);
buttonPanel.setVerticalAlignment(HorizontalPanel.ALIGN_MIDDLE);
buttonPanel.add(closeButton);
VerticalPanel dialogPanel = new VerticalPanel();
dialogPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
dialogPanel.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE);
Widget filePreview = generateFilePreview(node);
dialogPanel.clear();
dialogPanel.add(filePreview);
dialogPanel.add(buttonPanel);
dialogPanel.setWidth("300px");
dialogBox.setGlassEnabled(false);
dialogBox.setModal(false);
// Set the contents of the Widget
dialogBox.setWidget(dialogPanel);
dialogBox.center();
dialogBox.show();
}
/**
* Generate a file preview to display
*
* @param node
* @return widget
*/
private Widget generateFilePreview(ProjectNode node) {
String fileSuffix = node.getProjectId() + "/" + node.getFileId();
String fileUrl = StorageUtil.getFileUrl(node.getProjectId(), node.getFileId());
if (StorageUtil.isImageFile(fileSuffix)) { // Image Preview
String fileType = StorageUtil.getContentTypeForFilePath(fileSuffix);
// Support preview for file types that all major browser support
if (fileType.endsWith("png") || fileType.endsWith("jpeg") || fileType.endsWith("gif")
|| fileType.endsWith("bmp")) {
Image img = new Image(fileUrl);
img.getElement().getStyle().setProperty("maxWidth","600px");
return img;
}
} else if (StorageUtil.isAudioFile(fileSuffix)) { // Audio Preview
String fileType = StorageUtil.getContentTypeForFilePath(fileSuffix);
if (fileType.endsWith("mp3") || fileType.endsWith("wav") || fileType.endsWith("ogg")) {
return new HTML("<audio controls><source src='" + fileUrl + "' type='" + fileType
+ "'>" + MESSAGES.filePlaybackError() + "</audio>");
}
} else if (StorageUtil.isVideoFile(fileSuffix)) { // Video Preview
String fileType = StorageUtil.getContentTypeForFilePath(fileSuffix);
if (fileType.endsWith("mp4") || fileType.endsWith("webm")) {
return new HTML("<video width='320' height='240' controls> <source src='" + fileUrl
+ "' type='" + fileType + "'>" + MESSAGES.filePlaybackError() + "</video>");
}
}
return new HTML(MESSAGES.filePreviewError());
}
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
package com.google.appinventor.client.explorer.youngandroid; package com.google.appinventor.client.explorer.youngandroid;
import com.google.appinventor.client.Images;
import com.google.appinventor.client.Ode; import com.google.appinventor.client.Ode;
import static com.google.appinventor.client.Ode.MESSAGES; import static com.google.appinventor.client.Ode.MESSAGES;
import com.google.appinventor.client.explorer.project.Project; import com.google.appinventor.client.explorer.project.Project;
...@@ -18,12 +19,14 @@ import com.google.appinventor.shared.rpc.project.ProjectNode; ...@@ -18,12 +19,14 @@ import com.google.appinventor.shared.rpc.project.ProjectNode;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidAssetNode; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidAssetNode;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidAssetsFolder; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidAssetsFolder;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidProjectNode; import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidProjectNode;
import com.google.appinventor.shared.storage.StorageUtil;
import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Tree; import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem; import com.google.gwt.user.client.ui.TreeItem;
...@@ -93,6 +96,7 @@ public class AssetList extends Composite implements ProjectChangeListener { ...@@ -93,6 +96,7 @@ public class AssetList extends Composite implements ProjectChangeListener {
* Populate the asset tree with files from the project's assets folder. * Populate the asset tree with files from the project's assets folder.
*/ */
private void refreshAssetList() { private void refreshAssetList() {
final Images images = Ode.getImageBundle();
OdeLog.log("AssetList: refreshing for project " + projectId); OdeLog.log("AssetList: refreshing for project " + projectId);
assetList.clear(); assetList.clear();
...@@ -104,8 +108,20 @@ public class AssetList extends Composite implements ProjectChangeListener { ...@@ -104,8 +108,20 @@ public class AssetList extends Composite implements ProjectChangeListener {
if (nodeName.length() > 20) if (nodeName.length() > 20)
nodeName = nodeName.substring(0, 8) + "..." + nodeName.substring(nodeName.length() - 9, nodeName = nodeName.substring(0, 8) + "..." + nodeName.substring(nodeName.length() - 9,
nodeName.length()); nodeName.length());
TreeItem treeItem = new TreeItem(
new HTML("<span>" + nodeName + "</span>")); String fileSuffix = node.getProjectId() + "/" + node.getFileId();
String treeItemText = "<span style='cursor: pointer'>" + nodeName + "</span>";
if (StorageUtil.isImageFile(fileSuffix)) {
Image mediaIcon = new Image(images.mediaIconImg());
treeItemText = "<span>" + mediaIcon + nodeName + "</span>";
} else if (StorageUtil.isAudioFile(fileSuffix )) {
Image mediaIcon = new Image(images.mediaIconAudio());
treeItemText = "<span>" + mediaIcon + nodeName + "</span>";
} else {
Image mediaIcon = new Image(images.mediaIconVideo());
treeItemText = "<span>" + mediaIcon + nodeName + "</span>";
}
TreeItem treeItem = new TreeItem(new HTML(treeItemText));
// keep a pointer from the tree item back to the actual node // keep a pointer from the tree item back to the actual node
treeItem.setUserObject(node); treeItem.setUserObject(node);
assetList.addItem(treeItem); assetList.addItem(treeItem);
......
...@@ -27,6 +27,8 @@ public class Tracking { ...@@ -27,6 +27,8 @@ public class Tracking {
"DeleteProject-YA"; "DeleteProject-YA";
public static final String PROJECT_ACTION_DELETE_FILE_YA = PROJECT_ACTION_PREFIX + public static final String PROJECT_ACTION_DELETE_FILE_YA = PROJECT_ACTION_PREFIX +
"DeleteFile-YA"; "DeleteFile-YA";
public static final String PROJECT_ACTION_PREVIEW_FILE_YA = PROJECT_ACTION_PREFIX +
"PreviewFile-YA";
public static final String PROJECT_ACTION_BUILD_BARCODE_YA = PROJECT_ACTION_PREFIX + public static final String PROJECT_ACTION_BUILD_BARCODE_YA = PROJECT_ACTION_PREFIX +
"BuildBarcode-YA"; "BuildBarcode-YA";
public static final String PROJECT_ACTION_BUILD_DOWNLOAD_YA = PROJECT_ACTION_PREFIX + public static final String PROJECT_ACTION_BUILD_DOWNLOAD_YA = PROJECT_ACTION_PREFIX +
......
...@@ -103,10 +103,12 @@ public class StorageUtil { ...@@ -103,10 +103,12 @@ public class StorageUtil {
/** /**
* Returns the content type for the given filePath. * Returns the content type for the given filePath.
* References support media files listed at http://developer.android.com/guide/appendix/media-formats.html
*/ */
public static String getContentTypeForFilePath(String filePath) { public static String getContentTypeForFilePath(String filePath) {
filePath = filePath.toLowerCase(); filePath = filePath.toLowerCase();
// Image File Types
if (filePath.endsWith(".gif")) { if (filePath.endsWith(".gif")) {
return "image/gif"; return "image/gif";
} }
...@@ -116,10 +118,69 @@ public class StorageUtil { ...@@ -116,10 +118,69 @@ public class StorageUtil {
if (filePath.endsWith(".png")) { if (filePath.endsWith(".png")) {
return "image/png"; return "image/png";
} }
if (filePath.endsWith(".bmp")) {
return "image/bmp";
}
if (filePath.endsWith(".webp")) {
return "image/webp";
}
// Audio File Types
if (filePath.endsWith(".mp3")) {
return "audio/mp3";
}
if (filePath.endsWith(".m4a")) {
return "audio/m4a";
}
if (filePath.endsWith(".aac")) {
return "audio/aac";
}
if (filePath.endsWith(".wav")) {
return "audio/wav";
}
if (filePath.endsWith(".flac")) {
return "audio/flac";
}
if (filePath.endsWith(".mid") || filePath.endsWith(".xmf") || filePath.endsWith(".mxmf")) {
return "audio/midi";
}
if (filePath.endsWith(".ota")) {
return "audio/ota";
}
if (filePath.endsWith(".rtttl")) {
return "audio/rtttl";
}
if (filePath.endsWith(".rtx")) {
return "audio/rtx";
}
if (filePath.endsWith(".imy")) {
return "audio/imy";
}
if (filePath.endsWith(".ogg")) {
return "audio/ogg";
}
// Video File Types
if (filePath.endsWith(".mp4")) {
return "video/mp4";
}
if (filePath.endsWith(".3gp")) {
return "video/3gp";
}
if (filePath.endsWith(".ts")) {
return "video/ts";
}
if (filePath.endsWith(".webm")) {
return "video/webm";
}
if (filePath.endsWith(".mkv")) {
return "video/mkv";
}
if (filePath.endsWith(".svg")) { if (filePath.endsWith(".svg")) {
return "image/svg+xml"; return "image/svg+xml";
} }
// Other File Types
if (filePath.endsWith(".apk")) { if (filePath.endsWith(".apk")) {
return "application/vnd.android.package-archive"; return "application/vnd.android.package-archive";
} }
...@@ -152,6 +213,22 @@ public class StorageUtil { ...@@ -152,6 +213,22 @@ public class StorageUtil {
return contentType.startsWith("text/"); return contentType.startsWith("text/");
} }
/**
* Returns true if the given filePath refers an audio file.
*/
public static boolean isAudioFile(String filePath) {
String contentType = getContentTypeForFilePath(filePath);
return contentType.startsWith("audio/");
}
/**
* Returns true if the given filePath refers a video file.
*/
public static boolean isVideoFile(String filePath) {
String contentType = getContentTypeForFilePath(filePath);
return contentType.startsWith("video/");
}
/** /**
* Returns the URL for the given project file. * Returns the URL for the given project file.
*/ */
......
...@@ -625,8 +625,10 @@ select { ...@@ -625,8 +625,10 @@ select {
.gwt-Tree .gwt-TreeItem span { .gwt-Tree .gwt-TreeItem span {
padding: 8px 4px 4px 4px; padding: 8px 4px 4px 4px;
cursor: pointer;
} }
.gwt-Tree .gwt-TreeItem span:hover,
.gwt-Tree .gwt-TreeItem-selected span { .gwt-Tree .gwt-TreeItem-selected span {
background-color: #d2e0a6; background-color: #d2e0a6;
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment