diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/PreviewTexture.java b/src/hdskins/java/com/voxelmodpack/hdskins/PreviewTexture.java index e5430ee2..f92caf25 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/PreviewTexture.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/PreviewTexture.java @@ -12,16 +12,23 @@ public class PreviewTexture extends ThreadDownloadImageData { private String model; + private String fileUrl; + public PreviewTexture(@Nullable String model, String url, ResourceLocation fallbackTexture, @Nullable IImageBuffer imageBuffer) { super(null, url, fallbackTexture, imageBuffer); this.model = model == null ? "default" : model; + this.fileUrl = url; } public boolean isTextureUploaded() { return uploaded && this.getGlTextureId() > -1; } + public String getUrl() { + return fileUrl; + } + @Override public void deleteGlTexture() { super.deleteGlTexture(); diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java b/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java index b05e66b0..792a19f1 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java @@ -14,9 +14,12 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.mumfrey.liteloader.util.log.LiteLoaderLogger; import com.voxelmodpack.hdskins.HDSkinManager; import com.voxelmodpack.hdskins.PreviewTextureManager; +import com.voxelmodpack.hdskins.skins.NetClient; import com.voxelmodpack.hdskins.skins.SkinServer; import com.voxelmodpack.hdskins.skins.SkinUpload; import com.voxelmodpack.hdskins.skins.SkinUploadResponse; +import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFile; +import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFileFolder; import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFilePNG; import net.minecraft.client.Minecraft; import net.minecraft.client.audio.PositionedSoundRecord; @@ -32,6 +35,8 @@ import net.minecraft.item.ItemStack; import net.minecraft.util.EnumHand; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.MathHelper; + +import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.logging.log4j.LogManager; import org.lwjgl.BufferUtils; @@ -65,6 +70,7 @@ public class GuiSkins extends GameGui { private SkinServer gateway; private Button btnUpload; + private Button btnDownload; private Button btnClear; private Button btnModeSteve; @@ -93,7 +99,7 @@ public class GuiSkins extends GameGui { private int refreshCounter = 0; - private ThreadOpenFilePNG openFileThread; + private ThreadOpenFile openFileThread; private final Object skinLock = new Object(); @@ -180,6 +186,7 @@ public class GuiSkins extends GameGui { selectedSkin = pendingSkinFile; pendingSkinFile = null; btnUpload.enabled = true; + btnDownload.enabled = true; onSetLocalSkin(textureType); } } @@ -244,10 +251,14 @@ public class GuiSkins extends GameGui { sender.enabled = false; })).setEnabled(!mc.isFullScreen()); - addButton(btnUpload = new Button(width / 2 - 24, height / 2 - 10, 48, 20, "hdskins.options.chevy", sender -> { + addButton(btnUpload = new Button(width / 2 - 24, height / 2 - 20, 48, 20, "hdskins.options.chevy", sender -> { punchServer("hdskins.upload", selectedSkin.toURI()); })).setEnabled(canUpload()).setTooltip("hdskins.options.chevy.title"); + addButton(btnDownload = new Button(width / 2 - 24, height / 2 + 20, 48, 20, "hdskins.options.download", sender -> { + launchSkinDownload(sender); + })).setEnabled(canDownload()).setTooltip("hdskins.options.download.title"); + addButton(btnClear = new Button(width / 2 + 60, height - 27, 90, 20, "hdskins.options.clear", sender -> { if (remotePlayer.isTextureSetupComplete()) { punchServer("hdskins.request", null); @@ -570,12 +581,37 @@ public class GuiSkins extends GameGui { uploadingSkin = true; uploadMessage = format(uploadMsg); btnUpload.enabled = canUpload(); + btnDownload.enabled = canDownload(); gateway.uploadSkin(mc.getSession(), new SkinUpload(textureType, path, getMetadata())) .thenAccept(this::onUploadComplete) .exceptionally(this::onUploadFailure); } + private void launchSkinDownload(Button sender) { + sender.enabled = false; + String loc = remotePlayer.getLocal(textureType).getRemote().getUrl(); + + new NetClient("GET", loc).async(HDSkinManager.skinDownloadExecutor).thenAccept(response -> { + openFileThread = new ThreadOpenFileFolder(mc, format("hdskins.open.title"), (fileDialog, dialogResult) -> { + openFileThread = null; + sender.enabled = true; + if (dialogResult == 0) { + File out = fileDialog.getSelectedFile(); + + try { + out.createNewFile(); + + FileUtils.copyInputStreamToFile(response.getInputStream(), out); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + openFileThread.start(); + }); + } + private Map getMetadata() { return ImmutableMap.of("model", thinArmType ? "slim" : "default"); } @@ -587,6 +623,7 @@ public class GuiSkins extends GameGui { showMessage = true; uploadingSkin = false; btnUpload.enabled = canUpload(); + btnDownload.enabled = canDownload(); return null; } @@ -596,12 +633,17 @@ public class GuiSkins extends GameGui { pendingRemoteSkinRefresh = true; uploadingSkin = false; btnUpload.enabled = canUpload(); + btnDownload.enabled = canDownload(); } protected boolean canUpload() { return selectedSkin != null && !uploadingSkin && !pendingRemoteSkinRefresh; } + protected boolean canDownload() { + return remotePlayer.getLocal(textureType).hasRemote(); + } + CompletableFuture loadTextures(GameProfile profile) { return gateway.getPreviewTextures(profile).thenApply(PreviewTextureManager::new); } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/skins/NetClient.java b/src/hdskins/java/com/voxelmodpack/hdskins/skins/NetClient.java index 8f7a54c1..5f00580f 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/skins/NetClient.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/skins/NetClient.java @@ -11,6 +11,8 @@ import java.io.File; import java.io.IOException; import java.net.URI; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; /** * Ew. Why so many builders? >.< @@ -43,10 +45,16 @@ public class NetClient { public MoreHttpResponses send() throws IOException { HttpUriRequest request = rqBuilder.build(); - for (Map.Entry parameter : headers.entrySet()) { - request.addHeader(parameter.getKey(), parameter.getValue().toString()); + if (headers != null) { + for (Map.Entry parameter : headers.entrySet()) { + request.addHeader(parameter.getKey(), parameter.getValue().toString()); + } } return MoreHttpResponses.execute(HDSkinManager.httpClient, request); } + + public CompletableFuture async(Executor exec) { + return CallableFutures.asyncFailableFuture(this::send, exec); + } } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/upload/awt/ThreadOpenFileFolder.java b/src/hdskins/java/com/voxelmodpack/hdskins/upload/awt/ThreadOpenFileFolder.java new file mode 100644 index 00000000..09cdd43c --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/upload/awt/ThreadOpenFileFolder.java @@ -0,0 +1,32 @@ +package com.voxelmodpack.hdskins.upload.awt; + +import net.minecraft.client.Minecraft; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +/** + * Opens an awt "Open File" dialog that only accepts directories. + */ +public class ThreadOpenFileFolder extends ThreadOpenFile { + + public ThreadOpenFileFolder(Minecraft minecraft, String dialogTitle, IOpenFileCallback callback) + throws IllegalStateException { + super(minecraft, dialogTitle, callback); + } + + @Override + protected FileFilter getFileFilter() { + return new FileFilter() { + @Override + public String getDescription() { + return "Directories"; + } + + @Override + public boolean accept(File f) { + return f.isDirectory(); + } + }; + } +} diff --git a/src/hdskins/resources/assets/hdskins/lang/en_us.lang b/src/hdskins/resources/assets/hdskins/lang/en_us.lang index ecfa7c51..f9b987c5 100644 --- a/src/hdskins/resources/assets/hdskins/lang/en_us.lang +++ b/src/hdskins/resources/assets/hdskins/lang/en_us.lang @@ -24,6 +24,9 @@ hdskins.mode.skin=%s hdskins.options.chevy=>> hdskins.options.chevy.title=Upload Skin +hdskins.options.download=<< +hdskins.options.download.title=Save Skin + hdskins.options.close=Close hdskins.options.clear=Clear hdskins.options.browse=Browse