/* Copyright (c) 2013-2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.launcher.ui; import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Point2D; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.text.Text; import se.llbit.chunky.launcher.ChunkyLauncher; import se.llbit.chunky.launcher.DownloadStatus; import se.llbit.chunky.launcher.VersionInfo; import se.llbit.chunky.resources.SettingsDirectory; import se.llbit.util.Pair; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.ResourceBundle; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.stream.Collectors; public final class UpdateDialogController implements Initializable { private final VersionInfo versionInfo; private final File libDir; private final File versionsDir; private final ExecutorService threadPool; private final ChunkyLauncherController launcher; @FXML protected Text releaseInfo; @FXML protected TextArea releaseNotes; @FXML protected Button updateButton; @FXML protected Button cancelButton; @FXML protected TitledPane detailsPane; @FXML protected TableView<Pair<VersionInfo.Library, VersionInfo.LibraryStatus>> dependencies; @FXML protected TableColumn<Pair<VersionInfo.Library, VersionInfo.LibraryStatus>, String> libraryCol; @FXML protected TableColumn<Pair<VersionInfo.Library, VersionInfo.LibraryStatus>, VersionInfo.LibraryStatus> statusCol; @FXML protected TableColumn<Pair<VersionInfo.Library, VersionInfo.LibraryStatus>, String> sizeCol; @FXML protected ProgressIndicator busyIndicator; @FXML protected ProgressBar progress; @FXML protected Label updateComplete; @SuppressWarnings("ResultOfMethodCallIgnored") public UpdateDialogController(ChunkyLauncherController launcher, VersionInfo versionInfo) { this.launcher = launcher; this.versionInfo = versionInfo; File chunkyDir = SettingsDirectory.getSettingsDirectory(); libDir = new File(chunkyDir, "lib"); if (!libDir.isDirectory()) { libDir.mkdirs(); } versionsDir = new File(chunkyDir, "versions"); if (!versionsDir.isDirectory()) { versionsDir.mkdirs(); } threadPool = Executors.newFixedThreadPool(4, runnable -> { Thread thread = Executors.defaultThreadFactory().newThread(runnable); thread.setDaemon(true); return thread; }); } @Override public void initialize(URL location, ResourceBundle resources) { releaseInfo.setText(String.format("Version %s released on %s", versionInfo.name, versionInfo.date())); if (versionInfo.notes.isEmpty()) { releaseNotes.setText("No release notes available."); } else { releaseNotes.setText(versionInfo.notes); } updateButton.setOnAction(event -> downloadUpdate()); cancelButton.setOnAction(event -> cancelButton.getScene().getWindow().hide()); detailsPane.expandedProperty().addListener(observable -> Platform.runLater( () -> detailsPane.getScene().getWindow().sizeToScene())); dependencies.getItems().addAll(versionInfo.libraries.stream() .map(lib -> new Pair<>(lib, lib.testIntegrity(libDir))).collect(Collectors.toList())); libraryCol.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().thing1.name)); statusCol.setCellValueFactory(data -> new ReadOnlyObjectWrapper<>(data.getValue().thing2)); sizeCol.setCellValueFactory(data -> new ReadOnlyStringWrapper( ChunkyLauncher.prettyPrintSize(data.getValue().thing1.size))); statusCol.setCellFactory(param -> new TableCell<Pair<VersionInfo.Library, VersionInfo.LibraryStatus>, VersionInfo.LibraryStatus>() { @Override protected void updateItem(VersionInfo.LibraryStatus status, boolean empty) { if (status != null) { InputStream imageStream = null; switch (status) { case PASSED: case DOWNLOADED_OK: imageStream = getClass().getResourceAsStream("cached.png"); break; case MD5_MISMATCH: case MISSING: imageStream = getClass().getResourceAsStream("refresh.png"); break; case INCOMPLETE_INFO: case MALFORMED_URL: case FILE_NOT_FOUND: case DOWNLOAD_FAILED: imageStream = getClass().getResourceAsStream("failed.png"); break; } if (imageStream != null) { ImageView image = new ImageView(new Image(imageStream)); setGraphic(image); } setText(status.downloadStatus()); } } }); } private void downloadUpdate() { updateButton.setDisable(true); busyIndicator.setVisible(true); progress.setProgress(0); progress.setStyle(""); // Clear the progressbar style in case a previous download failed. if (!libDir.isDirectory()) { updateFailed(String.format("Library directory (%s) does not exist.", libDir.getAbsolutePath())); return; } if (!versionsDir.isDirectory()) { updateFailed(String.format("Versions directory (%s) does not exist.", libDir.getAbsolutePath())); return; } final List<Future<DownloadStatus>> results = new LinkedList<>(); List<Pair<VersionInfo.Library, VersionInfo.LibraryStatus>> toDownload = dependencies.getItems().stream().filter( item -> item.thing2 != VersionInfo.LibraryStatus.PASSED && item.thing2 != VersionInfo.LibraryStatus.INCOMPLETE_INFO) .collect(Collectors.toList()); double totalBytes = 0; for (Pair<VersionInfo.Library, VersionInfo.LibraryStatus> download : toDownload) { totalBytes += download.thing1.size; } final double finalTotalBytes = totalBytes; toDownload.stream().forEach(item -> results.add(threadPool.submit(new DownloadJob(item.thing1, () -> progress.setProgress( progress.getProgress() + item.thing1.size / finalTotalBytes))))); new Downloader(this, versionInfo, results).start(); } /** Waits for downloads to complete. */ static class Downloader extends Thread { private final Collection<Future<DownloadStatus>> results; private final UpdateDialogController controller; private final VersionInfo version; public Downloader(UpdateDialogController controller, VersionInfo version, Collection<Future<DownloadStatus>> resultFutures) { this.controller = controller; this.version = version; results = resultFutures; } @Override public void run() { boolean failed = false; for (Future<DownloadStatus> result : results) { try { if (result.get() != DownloadStatus.SUCCESS) { failed = true; } } catch (InterruptedException | ExecutionException e) { failed = true; } } if (failed) { controller.updateFailed("Failed to download some required libraries. Please try again later."); return; } try { File versionFile = new File(controller.versionsDir, version.name + ".json"); version.writeTo(versionFile); controller.downloadSucceeded(); } catch (IOException e) { controller.updateFailed("Failed to update version info. Please try again later."); } } } /** * Shows a tooltip with the given error message and sets the progress bar color * to red and sets max progress. */ private void updateFailed(final String message) { Platform.runLater(() -> { progress.setProgress(1.0); progress.setStyle("-fx-accent: red;"); Tooltip tooltip = new Tooltip(message); Point2D screen = progress.localToScreen(0, 0); tooltip.setAutoHide(true); tooltip.show(progress, screen.getX(), screen.getY() + progress.getHeight()); updateButton.setDisable(false); busyIndicator.setVisible(false); System.err.println(message); }); } /** * Changes the title of the "Cancel" button to "Close". * Changes the update button into a launch button. * Also shows the "Download completed!" label, and sets the * progress bar to max progress and sets the progress color to green. */ private void downloadSucceeded() { Platform.runLater(() -> { progress.setProgress(1.0); progress.setStyle("-fx-accent: green;"); updateComplete.setVisible(true); cancelButton.setText("Close"); updateButton.setDisable(false); updateButton.setText("Launch Chunky"); updateButton.setOnAction(event -> { updateButton.getScene().getWindow().hide(); launcher.launchChunky(); }); launcher.updateVersionList(); launcher.selectLatestVersion(); busyIndicator.setVisible(false); }); } class DownloadJob implements Callable<DownloadStatus> { private final VersionInfo.Library lib; private final Runnable callback; public DownloadJob(VersionInfo.Library lib, Runnable callback) { this.lib = lib; this.callback = callback; } @Override public DownloadStatus call() throws Exception { DownloadStatus result = null; // First try to download using the URL specified for the library. if (!lib.url.isEmpty()) { result = ChunkyLauncher.tryDownload(libDir, lib, lib.url); switch (result) { case MALFORMED_URL: System.err.println("Malformed URL: " + lib.url); break; case FILE_NOT_FOUND: System.err.println("File not found: " + lib.url); break; case DOWNLOAD_FAILED: System.err.println("Download failed: " + lib.url); break; default: break; } } // Using the library URL failed. // Try downloading the library from the default update site. String defaultUrl = "http://chunkyupdate.llbit.se/lib/" + lib.name; if (result != DownloadStatus.SUCCESS) { result = ChunkyLauncher.tryDownload(libDir, lib, defaultUrl); } switch (result) { case SUCCESS: updateDependencyStatus(lib, VersionInfo.LibraryStatus.DOWNLOADED_OK); break; case MALFORMED_URL: updateDependencyStatus(lib, VersionInfo.LibraryStatus.MALFORMED_URL); System.err.println("Malformed URL: " + defaultUrl); break; case FILE_NOT_FOUND: updateDependencyStatus(lib, VersionInfo.LibraryStatus.FILE_NOT_FOUND); System.err.println("File not found: " + defaultUrl); break; case DOWNLOAD_FAILED: updateDependencyStatus(lib, VersionInfo.LibraryStatus.DOWNLOAD_FAILED); System.err.println("Download failed: " + defaultUrl); break; } callback.run(); return result; } } /** Update status of a single library dependency in the table view. */ private void updateDependencyStatus(VersionInfo.Library lib, VersionInfo.LibraryStatus newStatus) { dependencies.getItems().setAll(dependencies.getItems().stream().map( item -> { if (item.thing1 != lib) { return item; } else { return new Pair<>(item.thing1, newStatus); } }).collect(Collectors.toList())); } }