diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java index 78330ff..2106a3d 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java @@ -17,6 +17,7 @@ public class Launcher extends Application { public static final Path BASE_DIR = Path.of(System.getProperty("user.home"), ".ace-of-shades"); public static final Path VERSIONS_DIR = BASE_DIR.resolve("versions"); public static final Path PROFILES_FILE = BASE_DIR.resolve("profiles.json"); + public static final Path JRE_PATH = BASE_DIR.resolve("jre"); @Override public void start(Stage stage) throws IOException { diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java index 7af9ab4..d1c5fa4 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java @@ -1,9 +1,5 @@ package nl.andrewl.aos2_launcher; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; @@ -15,9 +11,13 @@ import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.ProgressIndicator; import javafx.scene.input.MouseEvent; import javafx.scene.layout.VBox; import nl.andrewl.aos2_launcher.model.Profile; +import nl.andrewl.aos2_launcher.model.ProfileSet; +import nl.andrewl.aos2_launcher.model.ProgressReporter; import nl.andrewl.aos2_launcher.model.Server; import nl.andrewl.aos2_launcher.view.BindingUtil; import nl.andrewl.aos2_launcher.view.EditProfileDialog; @@ -25,37 +25,32 @@ import nl.andrewl.aos2_launcher.view.ProfileView; import nl.andrewl.aos2_launcher.view.ServerView; import java.io.IOException; -import java.nio.file.Files; import java.util.ArrayList; -public class MainViewController { - @FXML - public Button playButton; - @FXML - public Button editProfileButton; - @FXML - public Button removeProfileButton; - @FXML - public VBox profilesVBox; - @FXML - public VBox serversVBox; - @FXML - public Label selectedProfileLabel; - @FXML - public Label selectedServerLabel; +public class MainViewController implements ProgressReporter { + @FXML public Button playButton; + @FXML public Button editProfileButton; + @FXML public Button removeProfileButton; + @FXML public VBox profilesVBox; + @FXML public VBox serversVBox; + @FXML public Label selectedProfileLabel; + @FXML public Label selectedServerLabel; - private final ObservableList profiles = FXCollections.observableArrayList(); + @FXML public VBox progressVBox; + @FXML public Label progressLabel; + @FXML public ProgressBar progressBar; + + private final ProfileSet profileSet = new ProfileSet(); private final ObservableList servers = FXCollections.observableArrayList(); private final ObjectProperty selectedServer = new SimpleObjectProperty<>(null); - private final ObjectProperty selectedProfile = new SimpleObjectProperty<>(null); private final ServersFetcher serversFetcher = new ServersFetcher(); @FXML public void initialize() { BindingUtil.mapContent(serversVBox.getChildren(), servers, ServerView::new); - BindingUtil.mapContent(profilesVBox.getChildren(), profiles, ProfileView::new); - selectedProfile.addListener((observable, oldValue, newValue) -> { + BindingUtil.mapContent(profilesVBox.getChildren(), profileSet.getProfiles(), ProfileView::new); + profileSet.selectedProfileProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) { selectedProfileLabel.setText("None"); } else { @@ -69,16 +64,16 @@ public class MainViewController { selectedServerLabel.setText(newValue.getName()); } }); - BooleanBinding playBind = selectedProfile.isNull().or(selectedServer.isNull()); + BooleanBinding playBind = profileSet.selectedProfileProperty().isNull().or(selectedServer.isNull()); playButton.disableProperty().bind(playBind); - editProfileButton.disableProperty().bind(selectedProfile.isNull()); - removeProfileButton.disableProperty().bind(selectedProfile.isNull()); + editProfileButton.disableProperty().bind(profileSet.selectedProfileProperty().isNull()); + removeProfileButton.disableProperty().bind(profileSet.selectedProfileProperty().isNull()); profilesVBox.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { Node target = (Node) event.getTarget(); while (target != null) { if (target instanceof ProfileView view) { - if (view.getProfile().equals(selectedProfile.get()) && event.isControlDown()) { + if (view.getProfile().equals(profileSet.getSelectedProfile()) && event.isControlDown()) { selectProfile(null); } else if (!event.isControlDown() && event.getClickCount() == 2) { selectProfile(view); @@ -107,7 +102,10 @@ public class MainViewController { } selectServer(null); }); - loadProfiles(); + progressVBox.managedProperty().bind(progressVBox.visibleProperty()); + progressVBox.setVisible(false); + profileSet.loadOrCreateStandardFile(); + updateProfileViewSelectedClass(); refreshServers(); } @@ -127,101 +125,98 @@ public class MainViewController { @FXML public void addProfile() { EditProfileDialog dialog = new EditProfileDialog(profilesVBox.getScene().getWindow()); - dialog.showAndWait().ifPresent(profiles::add); - saveProfiles(); + dialog.showAndWait().ifPresent(profileSet::addNewProfile); } @FXML public void editProfile() { - EditProfileDialog dialog = new EditProfileDialog(profilesVBox.getScene().getWindow(), selectedProfile.get()); + EditProfileDialog dialog = new EditProfileDialog(profilesVBox.getScene().getWindow(), profileSet.getSelectedProfile()); dialog.showAndWait(); - saveProfiles(); + profileSet.save(); } @FXML public void removeProfile() { - if (selectedProfile.getValue() != null) { - profiles.remove(selectedProfile.getValue()); - saveProfiles(); - } + profileSet.removeSelectedProfile(); } @FXML public void play() { - Profile profile = this.selectedProfile.get(); + Profile profile = profileSet.getSelectedProfile(); Server server = this.selectedServer.get(); - VersionFetcher.INSTANCE.ensureVersionIsDownloaded(profile.getClientVersion()) - .thenAccept(path -> { - try { - Process p = new ProcessBuilder() - .command("java", "-jar", path.toAbsolutePath().toString()) - .directory(Launcher.BASE_DIR.toFile()) - .inheritIO() - .start(); - } catch (IOException e) { - e.printStackTrace(); - } - }); + SystemVersionValidator.getJreExecutablePath(this) + .thenAccept(jrePath -> { + VersionFetcher.INSTANCE.ensureVersionIsDownloaded(profile.getClientVersion(), this) + .thenAccept(clientJarPath -> { + try { + Process p = new ProcessBuilder() + .command( + jrePath.toAbsolutePath().toString(), + "-jar", + clientJarPath.toAbsolutePath().toString() + ) + .directory(Launcher.BASE_DIR.toFile()) + .inheritIO() + .start(); + p.wait(); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + }); } private void selectProfile(ProfileView view) { Profile profile = view == null ? null : view.getProfile(); - selectedProfile.set(profile); + profileSet.selectProfile(profile); + updateProfileViewSelectedClass(); + } + + private void updateProfileViewSelectedClass() { PseudoClass selectedClass = PseudoClass.getPseudoClass("selected"); for (var node : profilesVBox.getChildren()) { - node.pseudoClassStateChanged(selectedClass, false); - } - if (view != null) { - view.pseudoClassStateChanged(selectedClass, true); + ProfileView view = (ProfileView) node; + view.pseudoClassStateChanged(selectedClass, view.getProfile().equals(profileSet.getSelectedProfile())); } } private void selectServer(ServerView view) { Server server = view == null ? null : view.getServer(); selectedServer.set(server); + updateServerViewSelectedClass(); + } + + private void updateServerViewSelectedClass() { PseudoClass selectedClass = PseudoClass.getPseudoClass("selected"); for (var node : serversVBox.getChildren()) { - node.pseudoClassStateChanged(selectedClass, false); - } - if (view != null) { - view.pseudoClassStateChanged(selectedClass, true); + ServerView view = (ServerView) node; + view.pseudoClassStateChanged(selectedClass, view.getServer().equals(selectedServer.get())); } } - private void loadProfiles() { - if (!Files.exists(Launcher.PROFILES_FILE)) return; - try (var reader = Files.newBufferedReader(Launcher.PROFILES_FILE)) { - profiles.clear(); - JsonArray array = new Gson().fromJson(reader, JsonArray.class); - for (var element : array) { - if (element.isJsonObject()) { - JsonObject obj = element.getAsJsonObject(); - Profile profile = new Profile(); - profile.setName(obj.get("name").getAsString()); - profile.setDescription(obj.get("description").getAsString()); - profile.setClientVersion(obj.get("clientVersion").getAsString()); - profiles.add(profile); - } - } - } catch (IOException e) { - e.printStackTrace(); - } + @Override + public void enableProgress() { + Platform.runLater(() -> { + progressVBox.setVisible(true); + progressBar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS); + progressLabel.setText(null); + }); } - private void saveProfiles() { - JsonArray array = new JsonArray(profiles.size()); - for (var profile : profiles) { - JsonObject obj = new JsonObject(); - obj.addProperty("name", profile.getName()); - obj.addProperty("description", profile.getDescription()); - obj.addProperty("clientVersion", profile.getClientVersion()); - array.add(obj); - } - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - try (var writer = Files.newBufferedWriter(Launcher.PROFILES_FILE)) { - gson.toJson(array, writer); - } catch (IOException e) { - e.printStackTrace(); - } + @Override + public void disableProgress() { + Platform.runLater(() -> progressVBox.setVisible(false)); + } + + @Override + public void setActionText(String text) { + Platform.runLater(() -> progressLabel.setText(text)); + } + + @Override + public void setProgress(double progress) { + Platform.runLater(() -> progressBar.setProgress(progress)); } } diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java index 8517dec..01c087e 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java @@ -1,5 +1,21 @@ package nl.andrewl.aos2_launcher; +import nl.andrewl.aos2_launcher.model.ProgressReporter; +import nl.andrewl.aos2_launcher.util.FileUtils; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiPredicate; + public class SystemVersionValidator { private static final String os = System.getProperty("os.name").trim().toLowerCase(); private static final String arch = System.getProperty("os.arch").trim().toLowerCase(); @@ -15,6 +31,8 @@ public class SystemVersionValidator { private static final boolean ARCH_ARM = arch.equals("arm"); private static final boolean ARCH_ARM32 = arch.equals("arm32"); + private static final String JRE_DOWNLOAD_URL = "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.4+8/"; + public static String getPreferredVersionSuffix() { if (OS_LINUX) { if (ARCH_AARCH64) return "linux-aarch64"; @@ -32,4 +50,85 @@ public class SystemVersionValidator { System.err.println("Couldn't determine the preferred OS/ARCH version. Defaulting to windows-amd64."); return "windows-amd64"; } + + public static CompletableFuture getJreExecutablePath(ProgressReporter progressReporter) { + Optional optionalExecutablePath = findJreExecutable(); + return optionalExecutablePath.map(CompletableFuture::completedFuture) + .orElseGet(() -> downloadAppropriateJre(progressReporter)); + } + + public static CompletableFuture downloadAppropriateJre(ProgressReporter progressReporter) { + progressReporter.enableProgress(); + progressReporter.setActionText("Downloading JRE..."); + HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().GET().timeout(Duration.ofMinutes(5)); + String preferredJreName = getPreferredJreName(); + String url = JRE_DOWNLOAD_URL + preferredJreName; + HttpRequest req = requestBuilder.uri(URI.create(url)).build(); + return httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofInputStream()) + .thenApplyAsync(resp -> { + if (resp.statusCode() == 200) { + // Download sequentially, and update the progress. + try { + if (Files.exists(Launcher.JRE_PATH)) { + FileUtils.deleteRecursive(Launcher.JRE_PATH); + } + Files.createDirectory(Launcher.JRE_PATH); + Path jreArchiveFile = Launcher.JRE_PATH.resolve(preferredJreName); + FileUtils.downloadWithProgress(jreArchiveFile, resp, progressReporter); + progressReporter.setProgress(-1); // Indefinite progress. + progressReporter.setActionText("Unpacking JRE..."); + ProcessBuilder pb = new ProcessBuilder().inheritIO(); + if (OS_LINUX || OS_MAC) { + pb.command("tar", "-xzf", jreArchiveFile.toAbsolutePath().toString(), "-C", Launcher.JRE_PATH.toAbsolutePath().toString()); + } else if (OS_WINDOWS) { + pb.command("powershell", "-command", "\"Expand-Archive -Force '" + jreArchiveFile.toAbsolutePath() + "' '" + Launcher.JRE_PATH.toAbsolutePath() + "'\""); + } + Process process = pb.start(); + int result = process.waitFor(); + if (result != 0) throw new IOException("Archive extraction process exited with non-zero code: " + result); + Files.delete(jreArchiveFile); + progressReporter.setActionText("Looking for java executable..."); + Optional optionalExecutablePath = findJreExecutable(); + if (optionalExecutablePath.isEmpty()) throw new IOException("Couldn't find java executable."); + progressReporter.disableProgress(); + return optionalExecutablePath.get(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException("JRE download failed: " + resp.statusCode()); + } + }); + } + + private static Optional findJreExecutable() { + if (!Files.exists(Launcher.JRE_PATH)) return Optional.empty(); + BiPredicate pred = (path, basicFileAttributes) -> { + String filename = path.getFileName().toString(); + return Files.isExecutable(path) && (filename.equals("java") || filename.equals("java.exe")); + }; + try (var s = Files.find(Launcher.JRE_PATH, 3, pred)) { + return s.findFirst(); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + private static String getPreferredJreName() { + if (OS_LINUX) { + if (ARCH_AARCH64) return "OpenJDK17U-jre_aarch64_linux_hotspot_17.0.4_8.tar.gz"; + if (ARCH_AMD64) return "OpenJDK17U-jre_x64_linux_hotspot_17.0.4_8.tar.gz"; + if (ARCH_ARM || ARCH_ARM32) return "OpenJDK17U-jre_arm_linux_hotspot_17.0.4_8.tar.gz"; + } else if (OS_MAC) { + if (ARCH_AARCH64) return "OpenJDK17U-jre_aarch64_mac_hotspot_17.0.4_8.tar.gz"; + if (ARCH_X86_64) return "OpenJDK17U-jre_x64_mac_hotspot_17.0.4_8.tar.gz"; + } else if (OS_WINDOWS) { + if (ARCH_AARCH64 || ARCH_AMD64) return "OpenJDK17U-jre_x64_windows_hotspot_17.0.4_8.zip"; + if (ARCH_X86) return "OpenJDK17U-jre_x86-32_windows_hotspot_17.0.4_8.zip"; + } + System.err.println("Couldn't determine the preferred JRE version. Defaulting to x64_windows."); + return "OpenJDK17U-jre_x64_windows_hotspot_17.0.4_8.zip"; + } } diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java index 2152a03..dcd0845 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java @@ -4,6 +4,8 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import nl.andrewl.aos2_launcher.model.ClientVersionRelease; +import nl.andrewl.aos2_launcher.model.ProgressReporter; +import nl.andrewl.aos2_launcher.util.FileUtils; import java.io.IOException; import java.io.InputStreamReader; @@ -98,7 +100,7 @@ public class VersionFetcher { } } - public CompletableFuture ensureVersionIsDownloaded(String versionTag) { + public CompletableFuture ensureVersionIsDownloaded(String versionTag, ProgressReporter progressReporter) { try (var s = Files.list(Launcher.VERSIONS_DIR)) { Optional optionalFile = s.filter(f -> isVersionFile(f) && versionTag.equals(extractVersion(f))) .findFirst(); @@ -106,11 +108,15 @@ public class VersionFetcher { } catch (IOException e) { return CompletableFuture.failedFuture(e); } - return getRelease(versionTag) - .thenComposeAsync(this::downloadVersion); + progressReporter.enableProgress(); + progressReporter.setActionText("Downloading client " + versionTag + "..."); + var future = getRelease(versionTag) + .thenComposeAsync(release -> downloadVersion(release, progressReporter)); + future.thenRun(progressReporter::disableProgress); + return future; } - private CompletableFuture downloadVersion(ClientVersionRelease release) { + private CompletableFuture downloadVersion(ClientVersionRelease release, ProgressReporter progressReporter) { System.out.println("Downloading version " + release.tag()); HttpRequest req = HttpRequest.newBuilder(URI.create(release.assetsUrl())) .GET().timeout(Duration.ofSeconds(3)).build(); @@ -140,10 +146,19 @@ public class VersionFetcher { .GET().timeout(Duration.ofMinutes(5)).build(); Path file = Launcher.VERSIONS_DIR.resolve(fileName); System.out.printf("Downloading %s to %s.%n", fileName, file.toAbsolutePath()); - return httpClient.sendAsync(downloadRequest, HttpResponse.BodyHandlers.ofFile(file)) + return httpClient.sendAsync(downloadRequest, HttpResponse.BodyHandlers.ofInputStream()) .thenApplyAsync(resp -> { - System.out.println(resp); - return resp.body(); + if (resp.statusCode() == 200) { + // Download sequentially, and update the progress. + try { + FileUtils.downloadWithProgress(file, resp, progressReporter); + } catch (IOException e) { + throw new RuntimeException(e); + } + return file; + } else { + throw new RuntimeException("Version download failed."); + } }); }); } diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java index 51e65a2..85721c6 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java @@ -3,15 +3,27 @@ package nl.andrewl.aos2_launcher.model; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import java.util.UUID; + public class Profile { + private final UUID id; private final StringProperty name; private final StringProperty description; private final StringProperty clientVersion; public Profile() { - this.name = new SimpleStringProperty(""); - this.description = new SimpleStringProperty(null); - this.clientVersion = new SimpleStringProperty(null); + this(UUID.randomUUID(), "", null, null); + } + + public Profile(UUID id, String name, String description, String clientVersion) { + this.id = id; + this.name = new SimpleStringProperty(name); + this.description = new SimpleStringProperty(description); + this.clientVersion = new SimpleStringProperty(clientVersion); + } + + public UUID getId() { + return id; } public String getName() { diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProfileSet.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProfileSet.java new file mode 100644 index 0000000..e45b7ed --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProfileSet.java @@ -0,0 +1,141 @@ +package nl.andrewl.aos2_launcher.model; + +import com.google.gson.*; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import nl.andrewl.aos2_launcher.Launcher; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +/** + * Model for managing the set of profiles in the app. + */ +public class ProfileSet { + private final ObservableList profiles; + private final ObjectProperty selectedProfile; + private Path lastFileUsed = null; + + public ProfileSet() { + this.profiles = FXCollections.observableArrayList(); + this.selectedProfile = new SimpleObjectProperty<>(null); + } + + public ProfileSet(Path file) throws IOException { + this(); + load(file); + } + + public void addNewProfile(Profile profile) { + profiles.add(profile); + selectedProfile.set(profile); + save(); + } + + public void removeProfile(Profile profile) { + if (profile == null) return; + boolean removed = profiles.remove(profile); + if (removed) { + if (selectedProfile.get() != null && selectedProfile.get().equals(profile)) { + selectedProfile.set(null); + } + save(); + } + } + + public void removeSelectedProfile() { + removeProfile(getSelectedProfile()); + } + + public void selectProfile(Profile profile) { + if (!profiles.contains(profile)) return; + selectedProfile.set(profile); + } + + public void load(Path file) throws IOException { + try (var reader = Files.newBufferedReader(file)) { + JsonObject data = new Gson().fromJson(reader, JsonObject.class); + profiles.clear(); + JsonElement selectedProfileIdElement = data.get("selectedProfileId"); + UUID selectedProfileId = selectedProfileIdElement.isJsonNull() ? null : UUID.fromString(selectedProfileIdElement.getAsString()); + JsonArray profilesArray = data.getAsJsonArray("profiles"); + for (JsonElement element : profilesArray) { + JsonObject profileObj = element.getAsJsonObject(); + UUID id = UUID.fromString(profileObj.get("id").getAsString()); + String name = profileObj.get("name").getAsString(); + String description = profileObj.get("description").getAsString(); + String clientVersion = profileObj.get("clientVersion").getAsString(); + Profile profile = new Profile(id, name, description, clientVersion); + profiles.add(profile); + if (selectedProfileId != null && selectedProfileId.equals(profile.getId())) { + selectedProfile.set(profile); + } + } + lastFileUsed = file; + } + } + + public void loadOrCreateStandardFile() { + if (!Files.exists(Launcher.PROFILES_FILE)) { + try { + save(Launcher.PROFILES_FILE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + try { + load(Launcher.PROFILES_FILE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + } + + public void save(Path file) throws IOException { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + JsonObject data = new JsonObject(); + String selectedProfileId = selectedProfile.getValue() == null ? null : selectedProfile.getValue().getId().toString(); + data.addProperty("selectedProfileId", selectedProfileId); + JsonArray profilesArray = new JsonArray(profiles.size()); + for (Profile profile : profiles) { + JsonObject obj = new JsonObject(); + obj.addProperty("id", profile.getId().toString()); + obj.addProperty("name", profile.getName()); + obj.addProperty("description", profile.getDescription()); + obj.addProperty("clientVersion", profile.getClientVersion()); + profilesArray.add(obj); + } + data.add("profiles", profilesArray); + try (var writer = Files.newBufferedWriter(file)) { + gson.toJson(data, writer); + } + lastFileUsed = file; + } + + public void save() { + if (lastFileUsed != null) { + try { + save(lastFileUsed); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public ObservableList getProfiles() { + return profiles; + } + + public Profile getSelectedProfile() { + return selectedProfile.get(); + } + + public ObjectProperty selectedProfileProperty() { + return selectedProfile; + } +} diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProgressReporter.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProgressReporter.java new file mode 100644 index 0000000..91e6475 --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProgressReporter.java @@ -0,0 +1,8 @@ +package nl.andrewl.aos2_launcher.model; + +public interface ProgressReporter { + void enableProgress(); + void disableProgress(); + void setActionText(String text); + void setProgress(double progress); +} diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/util/FileUtils.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/util/FileUtils.java new file mode 100644 index 0000000..9768b65 --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/util/FileUtils.java @@ -0,0 +1,74 @@ +package nl.andrewl.aos2_launcher.util; + +import nl.andrewl.aos2_launcher.model.ProgressReporter; + +import java.io.IOException; +import java.io.InputStream; +import java.net.http.HttpResponse; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; + +public class FileUtils { + public static String humanReadableByteCountSI(long bytes) { + if (-1000 < bytes && bytes < 1000) { + return bytes + " B"; + } + CharacterIterator ci = new StringCharacterIterator("kMGTPE"); + while (bytes <= -999_950 || bytes >= 999_950) { + bytes /= 1000; + ci.next(); + } + return String.format("%.1f %cB", bytes / 1000.0, ci.current()); + } + + public static String humanReadableByteCountBin(long bytes) { + long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); + if (absB < 1024) { + return bytes + " B"; + } + long value = absB; + CharacterIterator ci = new StringCharacterIterator("KMGTPE"); + for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { + value >>= 10; + ci.next(); + } + value *= Long.signum(bytes); + return String.format("%.1f %ciB", value / 1024.0, ci.current()); + } + + public static void deleteRecursive(Path p) throws IOException { + Files.walkFileTree(p, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + public static void downloadWithProgress(Path outputFile, HttpResponse resp, ProgressReporter reporter) throws IOException { + reporter.setProgress(0); + long size = resp.headers().firstValueAsLong("Content-Length").orElse(1); + try (var out = Files.newOutputStream(outputFile); var in = resp.body()) { + byte[] buffer = new byte[8192]; + long bytesRead = 0; + while (bytesRead < size) { + int readCount = in.read(buffer); + out.write(buffer, 0, readCount); + bytesRead += readCount; + reporter.setProgress((double) bytesRead / size); + } + } + } +} diff --git a/launcher/src/main/resources/main_view.fxml b/launcher/src/main/resources/main_view.fxml index ef98412..9cf9053 100644 --- a/launcher/src/main/resources/main_view.fxml +++ b/launcher/src/main/resources/main_view.fxml @@ -1,51 +1,61 @@ + - + + + - -