From c8918c933ade1401c08d1719a950b70e83a9b034 Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Sat, 31 Jan 2015 16:15:31 +0100 Subject: [PATCH] Added File Uploading GUI, not Thread Safe --- .../replaymod/api/client/ApiClient.java | 27 -- .../replaymod/api/client/FileUploader.java | 104 +++++++ .../api/client/holders/Category.java | 16 ++ .../replaymod/events/GuiEventHandler.java | 5 +- .../replaymod/gui/GuiConstants.java | 12 +- .../replaymod/gui/online/GuiReplayCenter.java | 32 ++- .../replaymod/gui/online/GuiUploadFile.java | 264 ++++++++++++++++++ .../gui/replaymanager/GuiReplayManager.java | 78 +++--- .../replaymod/online/CountingHttpEntity.java | 77 +++++ .../replaymod/reflection/MCPNames.java | 2 - src/main/resources/default_thumb.jpg | Bin 0 -> 27743 bytes 11 files changed, 541 insertions(+), 76 deletions(-) create mode 100644 src/main/java/eu/crushedpixel/replaymod/api/client/FileUploader.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/online/CountingHttpEntity.java create mode 100644 src/main/resources/default_thumb.jpg diff --git a/src/main/java/eu/crushedpixel/replaymod/api/client/ApiClient.java b/src/main/java/eu/crushedpixel/replaymod/api/client/ApiClient.java index bc71a0fb..dcdd5f5e 100644 --- a/src/main/java/eu/crushedpixel/replaymod/api/client/ApiClient.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/client/ApiClient.java @@ -78,33 +78,6 @@ public class ApiClient { return info; } - public void uploadFile(String auth, File file, Category category) throws IOException, ApiException { - QueryBuilder builder = new QueryBuilder(ApiMethods.upload_file); - builder.put("auth", auth); - builder.put("category", category.getId()); - String url = builder.toString(); - - CloseableHttpClient client = new DefaultHttpClient(); - HttpPost post = new HttpPost(url); - - FileEntity entity = new FileEntity(file); - post.setEntity(entity); - HttpResponse response = client.execute(post); - - if(response.getStatusLine().getStatusCode() != 200) { - JsonElement element = jsonParser.parse(EntityUtils.toString(response.getEntity())); - try { - ApiError err = gson.fromJson(element, ApiError.class); - if(err.getDesc() != null) { - client.close(); - throw new ApiException(err); - } - } catch(Exception e) {} - } - - client.close(); - } - public void downloadFile(String auth, int file, File target) throws IOException { QueryBuilder builder = new QueryBuilder(ApiMethods.download_file); builder.put("auth", auth); diff --git a/src/main/java/eu/crushedpixel/replaymod/api/client/FileUploader.java b/src/main/java/eu/crushedpixel/replaymod/api/client/FileUploader.java new file mode 100644 index 00000000..e6151637 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/api/client/FileUploader.java @@ -0,0 +1,104 @@ +package eu.crushedpixel.replaymod.api.client; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +import eu.crushedpixel.replaymod.api.client.holders.Category; + +public class FileUploader { + private static Gson gson = new Gson(); + private static JsonParser jsonParser = new JsonParser(); + + private boolean uploading = false; + private long filesize; + private long current; + + private String attachmentName = "file"; + private String attachmentFileName = "file.mcpr"; + private String crlf = "\r\n"; + private String twoHyphens = "--"; + + private String boundary = "*****"; + //private CountingHttpEntity counter; + + public void uploadFile(String auth, String filename, File file, Category category) throws IOException, ApiException, RuntimeException { + if(uploading) throw new RuntimeException("FileUploader is already uploading"); + uploading = true; + + filesize = file.length(); + current = 0; + + String postData = "?auth="+auth+"&category="+category.getId()+"&name="+filename; + + String url = "http://ReplayMod.com/api/upload_file"+postData; + HttpURLConnection con = (HttpURLConnection)new URL(url).openConnection(); + con.setUseCaches(false); + con.setDoOutput(true); + con.setRequestMethod("POST"); + con.setRequestProperty("Connection", "Keep-Alive"); + con.setRequestProperty("Cache-Control", "no-cache"); + con.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + this.boundary); + + HashMap params = new HashMap(); + params.put("auth", auth); + params.put("name", filename); + params.put("category", category.getId()+""); + + DataOutputStream request = new DataOutputStream(con.getOutputStream()); + + request.writeBytes(this.twoHyphens + this.boundary + this.crlf); + request.writeBytes("Content-Disposition: form-data; name=\"" + this.attachmentName + "\";filename=\"" + this.attachmentFileName + "\"" + this.crlf); + request.writeBytes(this.crlf); + + byte[] buf = new byte[1024]; + FileInputStream fis = new FileInputStream(file); + int len; + while((len = fis.read(buf)) != -1) { + request.write(buf); + current += len; + } + fis.close(); + + request.writeBytes(this.crlf); + request.writeBytes(this.twoHyphens + this.boundary + this.twoHyphens + this.crlf); + + request.flush(); + request.close(); + + int responseCode = ((HttpURLConnection)con).getResponseCode(); + InputStream is = null; + if(responseCode == 200) { + is = con.getInputStream(); + } else { + is = con.getErrorStream(); + } + + BufferedReader r = new BufferedReader(new InputStreamReader(is)); + + while(r.ready()) { + System.out.println(r.readLine()); + } + + System.out.println(responseCode); // Should be 200 + + con.disconnect(); + + uploading = false; + } + + public float getUploadProgress() { + if(!uploading) return 0.1f; + return (float)((double)current/(double)filesize); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/api/client/holders/Category.java b/src/main/java/eu/crushedpixel/replaymod/api/client/holders/Category.java index 8794aedf..4aa82c01 100644 --- a/src/main/java/eu/crushedpixel/replaymod/api/client/holders/Category.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/client/holders/Category.java @@ -20,4 +20,20 @@ public enum Category { } return null; } + + public String toNiceString() { + return (""+this).charAt(0)+(""+this).substring(1).toLowerCase(); + } + + public Category next() { + for(int i=0; i buttonBar = new ArrayList(); - GuiButton overviewButton = new GuiButton(GuiConstants.CENTER_OVERVIEW_BUTTON, 20, 30, "Overview"); - buttonBar.add(overviewButton); + GuiButton recentButton = new GuiButton(GuiConstants.CENTER_RECENT_BUTTON, 20, 30, "Newest Replays"); + buttonBar.add(recentButton); + + GuiButton bestButton = new GuiButton(GuiConstants.CENTER_BEST_BUTTON, 20, 30, "Best Replays"); + buttonBar.add(bestButton); GuiButton ownReplayButton = new GuiButton(GuiConstants.CENTER_MY_REPLAYS_BUTTON, 20, 30, "My Replays"); buttonBar.add(ownReplayButton); - GuiButton uploadReplayButton = new GuiButton(GuiConstants.CENTER_UPLOAD_BUTTON, 20, 30, "Upload"); - buttonBar.add(uploadReplayButton); + GuiButton searchButton = new GuiButton(GuiConstants.CENTER_SEARCH_BUTTON, 20, 30, "Search"); + buttonBar.add(searchButton); int i = 0; for(GuiButton b : buttonBar) { @@ -64,8 +68,11 @@ public class GuiReplayCenter extends GuiScreen implements GuiYesNoCallback { //Bottom Button Bar (dat alliteration) List bottomBar = new ArrayList(); - GuiButton exitButton = new GuiButton(GuiConstants.CENTER_BACK_BUTTON, 20, 20, "Back"); + GuiButton exitButton = new GuiButton(GuiConstants.CENTER_BACK_BUTTON, 20, 20, "Main Menu"); bottomBar.add(exitButton); + + GuiButton managerButton = new GuiButton(GuiConstants.CENTER_MANAGER_BUTTON, 20, 20, "Replay Manager"); + bottomBar.add(managerButton); GuiButton logoutButton = new GuiButton(GuiConstants.CENTER_LOGOUT_BUTTON, 20, 20, "Logout"); bottomBar.add(logoutButton); @@ -85,7 +92,7 @@ public class GuiReplayCenter extends GuiScreen implements GuiYesNoCallback { i++; } - showOverview(); + showOnlineRecent(); } @Override @@ -93,9 +100,18 @@ public class GuiReplayCenter extends GuiScreen implements GuiYesNoCallback { if(!button.enabled) return; if(button.id == GuiConstants.CENTER_BACK_BUTTON) { mc.displayGuiScreen(new GuiMainMenu()); - return; } else if(button.id == GuiConstants.CENTER_LOGOUT_BUTTON) { mc.displayGuiScreen(getYesNoGui(this, LOGOUT_CALLBACK_ID)); + } else if(button.id == GuiConstants.CENTER_MANAGER_BUTTON) { + mc.displayGuiScreen(new GuiReplayManager()); + } else if(button.id == GuiConstants.CENTER_RECENT_BUTTON) { + showOnlineRecent(); + } else if(button.id == GuiConstants.CENTER_BEST_BUTTON) { + + } else if(button.id == GuiConstants.CENTER_MY_REPLAYS_BUTTON) { + + } else if(button.id == GuiConstants.CENTER_SEARCH_BUTTON) { + } } @@ -136,7 +152,7 @@ public class GuiReplayCenter extends GuiScreen implements GuiYesNoCallback { Keyboard.enableRepeatEvents(false); } - public void showOverview() { + public void showOnlineRecent() { mc.addScheduledTask(new Runnable() { @Override public void run() { diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java new file mode 100644 index 00000000..c41f7b88 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java @@ -0,0 +1,264 @@ +package eu.crushedpixel.replaymod.gui.online; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiTextField; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.util.ResourceLocation; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.io.FilenameUtils; +import org.lwjgl.input.Keyboard; + +import com.google.gson.Gson; + +import eu.crushedpixel.replaymod.api.client.ApiException; +import eu.crushedpixel.replaymod.api.client.FileUploader; +import eu.crushedpixel.replaymod.api.client.holders.Category; +import eu.crushedpixel.replaymod.gui.GuiConstants; +import eu.crushedpixel.replaymod.gui.replaymanager.ResourceHelper; +import eu.crushedpixel.replaymod.online.authentication.AuthenticationHandler; +import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; +import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import eu.crushedpixel.replaymod.reflection.MCPNames; +import eu.crushedpixel.replaymod.utils.ImageUtils; + +public class GuiUploadFile extends GuiScreen { + + private GuiTextField fileTitleInput; + private GuiButton categoryButton, startUploadButton, cancelUploadButton, backButton; + + private Gson gson = new Gson(); + + private File replayFile; + private ReplayMetaData metaData; + private BufferedImage thumb; + + private FileUploader uploader = new FileUploader(); + + private Category category = Category.MINIGAME; + + private final ResourceLocation textureResource; + private DynamicTexture dynTex = null; + + private Minecraft mc = Minecraft.getMinecraft(); + + public GuiUploadFile(File file) { + this.textureResource = new ResourceLocation("upload_thumbs/"+FilenameUtils.getBaseName(file.getAbsolutePath())); + dynTex = null; + + boolean correctFile = false; + this.replayFile = file; + + if(("."+FilenameUtils.getExtension(file.getAbsolutePath())).equals(ConnectionEventHandler.ZIP_FILE_EXTENSION)) { + ZipFile archive = null; + try { + archive = new ZipFile(file); + ZipArchiveEntry recfile = archive.getEntry("recording"+ConnectionEventHandler.TEMP_FILE_EXTENSION); + ZipArchiveEntry metadata = archive.getEntry("metaData"+ConnectionEventHandler.JSON_FILE_EXTENSION); + + ZipArchiveEntry image = archive.getEntry("thumb"); + BufferedImage img = null; + if(image != null) { + InputStream is = archive.getInputStream(image); + is.skip(7); + BufferedImage bimg = ImageIO.read(is); + if(bimg != null) { + thumb = ImageUtils.scaleImage(bimg, new Dimension(1280, 720)); + } + } + + InputStream is = archive.getInputStream(metadata); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String json = br.readLine(); + + metaData = gson.fromJson(json, ReplayMetaData.class); + + archive.close(); + correctFile = true; + } catch(Exception e) { + e.printStackTrace(); + } finally { + if(archive != null) { + try { + archive.close(); + } catch (IOException e) {} + } + } + } + + if(!correctFile) { + System.out.println("Invalid file provided to upload"); + mc.displayGuiScreen(new GuiMainMenu()); //TODO: Error message + replayFile = null; + return; + } + + //If thumb is null, set image to placeholder + if(thumb == null) { + try { + thumb = ImageIO.read(MCPNames.class.getClassLoader().getResourceAsStream("default_thumb.jpg")); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void initGui() { + if(replayFile == null) return; + + fileTitleInput = new GuiTextField(GuiConstants.UPLOAD_NAME_INPUT, fontRendererObj, 200, 21, 120, 20); + String fname = FilenameUtils.getBaseName(replayFile.getAbsolutePath()); + fileTitleInput.setText(fname); + + + categoryButton = new GuiButton(GuiConstants.UPLOAD_CATEGORY_BUTTON, 200, 80, "Category: "+category.toNiceString()); + categoryButton.width = 120; + buttonList.add(categoryButton); + + List bottomBar = new ArrayList(); + startUploadButton = new GuiButton(GuiConstants.UPLOAD_START_BUTTON, 0, 0, "Start Upload"); + bottomBar.add(startUploadButton); + + cancelUploadButton = new GuiButton(GuiConstants.UPLOAD_CANCEL_BUTTON, 0, 0, "Cancel Upload"); + cancelUploadButton.enabled = false; + bottomBar.add(cancelUploadButton); + + backButton = new GuiButton(GuiConstants.UPLOAD_BACK_BUTTON, 0, 0, "Back"); + bottomBar.add(backButton); + + int i = 0; + for(GuiButton b : bottomBar) { + int w = this.width - 30; + int w2 = w/bottomBar.size(); + + int x = 15+(w2*i); + b.xPosition = x; + b.yPosition = height-30; + b.width = w2-4; + + buttonList.add(b); + + i++; + } + + startUploadButton.enabled = (!(fileTitleInput.getText().trim().length() < 5 || fileTitleInput.getText().trim().length() > 30 || + p.matcher(fileTitleInput.getText()).find())); + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + if(!button.enabled) return; + if(button.id == GuiConstants.UPLOAD_CATEGORY_BUTTON) { + category = category.next(); + categoryButton.displayString = "Category: "+category.toNiceString(); + } else if(button.id == GuiConstants.UPLOAD_BACK_BUTTON) { + mc.displayGuiScreen(new GuiMainMenu()); + } else if(button.id == GuiConstants.UPLOAD_START_BUTTON) { + mc.addScheduledTask(new Runnable() { + @Override + public void run() { + try { + uploader.uploadFile(AuthenticationHandler.getKey(), fileTitleInput.getText().trim(), replayFile, category); + } catch (ApiException e) { //TODO: Error handling + e.printStackTrace(); + mc.displayGuiScreen(new GuiMainMenu()); + } catch (RuntimeException e) { + e.printStackTrace(); + mc.displayGuiScreen(new GuiMainMenu()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.drawDefaultBackground(); + + drawString(fontRendererObj, metaData.getServerName(), 200, 50, Color.GRAY.getRGB()); + drawString(fontRendererObj, "Duration: "+String.format("%02dm%02ds", + TimeUnit.MILLISECONDS.toMinutes(metaData.getDuration()), + TimeUnit.MILLISECONDS.toSeconds(metaData.getDuration()) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(metaData.getDuration())) + ), 200, 65, Color.GRAY.getRGB()); + + drawCenteredString(fontRendererObj, "Upload File", this.width/2, 5, Color.WHITE.getRGB()); + + //Draw thumbnail + if(thumb != null) { + if(dynTex == null) { + dynTex = new DynamicTexture(thumb); + mc.getTextureManager().loadTexture(textureResource, dynTex); + dynTex.updateDynamicTexture(); + ResourceHelper.registerResource(textureResource); + } + + mc.getTextureManager().bindTexture(textureResource); //Will be freed by the ResourceHelper + Gui.drawScaledCustomSizeModalRect(20, 20, 0, 0, 1280, 720, 57*3, 32*3, 1280, 720); + } + + fileTitleInput.drawTextBox(); + + super.drawScreen(mouseX, mouseY, partialTicks); + + this.drawRect(20, this.height-100, width-20, this.height-80, Color.BLACK.getRGB()); + this.drawRect(22, this.height-98, width-22, this.height-82, Color.WHITE.getRGB()); + + int width = this.width-22 - 22; + float w = width*uploader.getUploadProgress(); + + this.drawRect(22, this.height-98, Math.round(22+w), this.height-82, Color.RED.getRGB()); + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + super.mouseClicked(mouseX, mouseY, mouseButton); + fileTitleInput.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + public void updateScreen() { + fileTitleInput.updateCursorCounter(); + } + + @Override + public void onGuiClosed() { + Keyboard.enableRepeatEvents(false); + } + + private static final Pattern p = Pattern.compile("[^a-z0-9 \\-_]", Pattern.CASE_INSENSITIVE); + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + if(fileTitleInput.isFocused()) { + fileTitleInput.textboxKeyTyped(typedChar, keyCode); + } + + startUploadButton.enabled = (!(fileTitleInput.getText().trim().length() < 5 || fileTitleInput.getText().trim().length() > 30 || + p.matcher(fileTitleInput.getText()).find())); + + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replaymanager/GuiReplayManager.java b/src/main/java/eu/crushedpixel/replaymod/gui/replaymanager/GuiReplayManager.java index 392887d3..eb915ed9 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replaymanager/GuiReplayManager.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/replaymanager/GuiReplayManager.java @@ -38,8 +38,11 @@ import com.google.gson.Gson; import com.mojang.realmsclient.util.Pair; import eu.crushedpixel.replaymod.gui.GuiReplaySettings; +import eu.crushedpixel.replaymod.gui.online.GuiUploadFile; +import eu.crushedpixel.replaymod.online.authentication.AuthenticationHandler; import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import eu.crushedpixel.replaymod.reflection.MCPNames; import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.ImageUtils; @@ -53,20 +56,21 @@ public class GuiReplayManager extends GuiScreen implements GuiYesNoCallback { private boolean initialized; private GuiReplayListExtended replayGuiList; private List, BufferedImage>> replayFileList = new ArrayList, BufferedImage>>(); - private GuiButton loadButton, folderButton, renameButton, deleteButton, cancelButton, settingsButton; + private GuiButton loadButton, uploadButton, folderButton, renameButton, deleteButton, cancelButton, settingsButton; private static Gson gson = new Gson(); private boolean replaying = false; private static final int LOAD_BUTTON_ID = 9001; - private static final int FOLDER_BUTTON_ID = 9002; - private static final int RENAME_BUTTON_ID = 9003; - private static final int DELETE_BUTTON_ID = 9004; - private static final int SETTINGS_BUTTON_ID = 9005; - private static final int CANCEL_BUTTON_ID = 9006; + private static final int UPLOAD_BUTTON_ID = 9002; + private static final int FOLDER_BUTTON_ID = 9003; + private static final int RENAME_BUTTON_ID = 9004; + private static final int DELETE_BUTTON_ID = 9005; + private static final int SETTINGS_BUTTON_ID = 9006; + private static final int CANCEL_BUTTON_ID = 9007; private boolean delete_file = false; - + private void reloadFiles() { replayGuiList.clearEntries(); replayFileList = new ArrayList, BufferedImage>>(); @@ -91,6 +95,11 @@ public class GuiReplayManager extends GuiScreen implements GuiYesNoCallback { img = ImageUtils.scaleImage(bimg, new Dimension(1280, 720)); } } + + //If thumb is null, set image to placeholder + if(img == null) { + img = ImageIO.read(MCPNames.class.getClassLoader().getResourceAsStream("default_thumb.jpg")); + } InputStream is = archive.getInputStream(metadata); BufferedReader br = new BufferedReader(new InputStreamReader(is)); @@ -146,7 +155,8 @@ public class GuiReplayManager extends GuiScreen implements GuiYesNoCallback { } private void createButtons() { - this.buttonList.add(loadButton = new GuiButton(LOAD_BUTTON_ID, this.width / 2 - 154, this.height - 52, 150, 20, I18n.format("Load Replay", new Object[0]))); + this.buttonList.add(loadButton = new GuiButton(LOAD_BUTTON_ID, this.width / 2 - 154, this.height - 52, 73, 20, I18n.format("Load", new Object[0]))); + this.buttonList.add(uploadButton = new GuiButton(UPLOAD_BUTTON_ID, this.width / 2 - 154 + 78, this.height - 52, 73, 20, I18n.format("Upload", new Object[0]))); this.buttonList.add(folderButton = new GuiButton(FOLDER_BUTTON_ID, this.width / 2 + 4, this.height - 52, 150, 20, I18n.format("Open Replay Folder...", new Object[0]))); this.buttonList.add(renameButton = new GuiButton(RENAME_BUTTON_ID, this.width / 2 - 154, this.height - 28, 72, 20, I18n.format("Rename", new Object[0]))); this.buttonList.add(deleteButton = new GuiButton(DELETE_BUTTON_ID, this.width / 2 - 76, this.height - 28, 72, 20, I18n.format("Delete", new Object[0]))); @@ -185,20 +195,17 @@ public class GuiReplayManager extends GuiScreen implements GuiYesNoCallback { @Override protected void actionPerformed(GuiButton button) throws IOException { - if (button.enabled) { + if(button.enabled) { if(button.id == LOAD_BUTTON_ID) { loadReplay(replayGuiList.selected); } - else if (button.id == CANCEL_BUTTON_ID) - { + else if(button.id == CANCEL_BUTTON_ID) { mc.displayGuiScreen(parentScreen); } - else if (button.id == DELETE_BUTTON_ID) - { + else if(button.id == DELETE_BUTTON_ID) { String s = replayGuiList.getListEntry(replayGuiList.selected).getFileName(); - if (s != null) - { + if (s != null) { delete_file = true; GuiYesNo guiyesno = getYesNoGui(this, s, 1); this.mc.displayGuiScreen(guiyesno); @@ -207,53 +214,48 @@ public class GuiReplayManager extends GuiScreen implements GuiYesNoCallback { else if(button.id == SETTINGS_BUTTON_ID) { this.mc.displayGuiScreen(new GuiReplaySettings(this)); } - else if(button.id == RENAME_BUTTON_ID) - { + else if(button.id == RENAME_BUTTON_ID) { File file = replayFileList.get(replayGuiList.selected).first().first(); this.mc.displayGuiScreen(new GuiRenameReplay(this, file)); } - else if(button.id == FOLDER_BUTTON_ID) - { + else if(button.id == UPLOAD_BUTTON_ID) { + File file = replayFileList.get(replayGuiList.selected).first().first(); + this.mc.displayGuiScreen(new GuiUploadFile(file)); + } + else if(button.id == FOLDER_BUTTON_ID) { File file1 = new File("./replay_recordings/"); file1.mkdirs(); String s = file1.getAbsolutePath(); - if (Util.getOSType() == Util.EnumOS.OSX) - { - try - { + if(Util.getOSType() == Util.EnumOS.OSX) { + try { Runtime.getRuntime().exec(new String[] {"/usr/bin/open", s}); return; } - catch (IOException ioexception1) {} + catch(IOException ioexception1) {} } - else if (Util.getOSType() == Util.EnumOS.WINDOWS) - { + else if(Util.getOSType() == Util.EnumOS.WINDOWS) { String s1 = String.format("cmd.exe /C start \"Open file\" \"%s\"", new Object[] {s}); - try - { + try{ Runtime.getRuntime().exec(s1); return; } - catch (IOException ioexception) {} + catch(IOException ioexception) {} } boolean flag = false; - try - { + try { Class oclass = Class.forName("java.awt.Desktop"); Object object = oclass.getMethod("getDesktop", new Class[0]).invoke((Object)null, new Object[0]); oclass.getMethod("browse", new Class[] {URI.class}).invoke(object, new Object[] {file1.toURI()}); } - catch (Throwable throwable) - { + catch(Throwable throwable) { flag = true; } - if (flag) - { + if(flag) { Sys.openURL("file://" + s); } } @@ -286,6 +288,12 @@ public class GuiReplayManager extends GuiScreen implements GuiYesNoCallback { public void setButtonsEnabled(boolean b) { loadButton.enabled = b; + if(!b || !AuthenticationHandler.isAuthenticated()) { + uploadButton.enabled = false; + } else { + uploadButton.enabled = true; + } + renameButton.enabled = b; deleteButton.enabled = b; } diff --git a/src/main/java/eu/crushedpixel/replaymod/online/CountingHttpEntity.java b/src/main/java/eu/crushedpixel/replaymod/online/CountingHttpEntity.java new file mode 100644 index 00000000..fbf521bb --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/online/CountingHttpEntity.java @@ -0,0 +1,77 @@ +package eu.crushedpixel.replaymod.online; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.http.entity.ContentType; +import org.apache.http.entity.FileEntity; + +public class CountingHttpEntity extends FileEntity { + + private OutputStreamProgress outstream; + + public class OutputStreamProgress extends OutputStream { + + private final OutputStream outstream; + private volatile long bytesWritten=0; + + public OutputStreamProgress(OutputStream outstream) { + this.outstream = outstream; + } + + @Override + public void write(int b) throws IOException { + outstream.write(b); + bytesWritten++; + } + + @Override + public void write(byte[] b) throws IOException { + outstream.write(b); + bytesWritten += b.length; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + outstream.write(b, off, len); + bytesWritten += len; + } + + @Override + public void flush() throws IOException { + outstream.flush(); + } + + @Override + public void close() throws IOException { + outstream.close(); + } + + public long getWrittenLength() { + return bytesWritten; + } + } + + public CountingHttpEntity(File file, ContentType type) { + super(file, type); + } + + @Override + public void writeTo(OutputStream outstream) throws IOException { + this.outstream = new OutputStreamProgress(outstream); + super.writeTo(this.outstream); + } + + public float getProgress() { + if (outstream == null) { + return 0; + } + long contentLength = getContentLength(); + if (contentLength <= 0) { + return 0; + } + long writtenLength = outstream.getWrittenLength(); + return (writtenLength/contentLength); + } +} \ No newline at end of file diff --git a/src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java b/src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java index e8f2e18e..b0f33c90 100644 --- a/src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java +++ b/src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java @@ -37,8 +37,6 @@ public final class MCPNames { static { if (use()) { String mappingsDir = "./../build/unpacked/mappings/"; - ResourceLocation fieldsLocation = new ResourceLocation("assets/replaymod/fields.csv"); - ResourceLocation methodsLocation = new ResourceLocation("assets/replaymod/methods.csv"); InputStream fieldsIs = MCPNames.class.getClassLoader().getResourceAsStream("fields.csv"); InputStream methodsIs = MCPNames.class.getClassLoader().getResourceAsStream("methods.csv"); diff --git a/src/main/resources/default_thumb.jpg b/src/main/resources/default_thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fbe127624187dbc22bb1c99e42ff41875abcb90a GIT binary patch literal 27743 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 d^U(AZ^)Uc^S;GKC!vCYMWB