/* * Copyright (c) 2014 Dennis Fischer. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0+ * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: Dennis Fischer */ package de.chaosfisch.uploader.gui.renderer; import com.cathive.fx.guice.GuiceFXMLLoader; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import de.chaosfisch.google.youtube.upload.IUploadService; import de.chaosfisch.google.youtube.upload.Status; import de.chaosfisch.google.youtube.upload.Upload; import de.chaosfisch.google.youtube.upload.events.UploadJobProgressEvent; import de.chaosfisch.services.ExtendedPlaceholders; import de.chaosfisch.uploader.gui.controller.ConfirmDialogController; import de.chaosfisch.uploader.gui.controller.UploadController; import de.chaosfisch.util.DesktopUtil; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.SceneBuilder; import javafx.scene.control.*; import javafx.scene.input.*; import javafx.scene.layout.HBox; import javafx.scene.layout.HBoxBuilder; import javafx.scene.layout.VBox; import javafx.scene.layout.VBoxBuilder; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageBuilder; import javafx.stage.StageStyle; import javafx.util.Callback; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.ResourceBundle; import java.util.TimeZone; import java.util.concurrent.TimeUnit; public class QueueUploadCellRenderer implements Callback<ListView<Upload>, ListCell<Upload>> { private final UploadController uploadController; private final EventBus eventBus; private final ResourceBundle resources; private final DesktopUtil desktopUtil; private final IUploadService uploadService; private final GuiceFXMLLoader fxmlLoader; private final DialogHelper dialogHelper; private final Provider<ProgressNodeRenderer> progressNodeProvider; private final ExtendedPlaceholders extendedPlaceholders; private static final Logger logger = LoggerFactory.getLogger(QueueUploadCellRenderer.class); @SuppressWarnings("WeakerAccess") @Inject public QueueUploadCellRenderer(final UploadController uploadController, final EventBus eventBus, @Named("i18n-resources") final ResourceBundle resources, final DesktopUtil desktopUtil, final IUploadService uploadService, final GuiceFXMLLoader fxmlLoader, final DialogHelper dialogHelper, final Provider<ProgressNodeRenderer> progressNodeProvider, final ExtendedPlaceholders extendedPlaceholders) { this.uploadController = uploadController; this.eventBus = eventBus; this.resources = resources; this.desktopUtil = desktopUtil; this.uploadService = uploadService; this.fxmlLoader = fxmlLoader; this.dialogHelper = dialogHelper; this.progressNodeProvider = progressNodeProvider; this.extendedPlaceholders = extendedPlaceholders; } @Override public ListCell<Upload> call(final ListView<Upload> uploadListView) { return new QueueUploadCell(); } private EventHandler<MouseEvent> createDragDetectedHandler(final ListCell<Upload> cell) { return new EventHandler<MouseEvent>() { @Override public void handle(final MouseEvent event) { final Dragboard db = cell.startDragAndDrop(TransferMode.MOVE); final ClipboardContent content = new ClipboardContent(); content.putString(String.valueOf(cell.getIndex())); db.setContent(content); } }; } private EventHandler<DragEvent> createDragOverHandler(final ListCell<Upload> cell, final ListView<Upload> listView) { return new EventHandler<DragEvent>() { @Override public void handle(final DragEvent event) { final Dragboard dragboard = event.getDragboard(); if (dragboard.hasString()) { final String value = dragboard.getString(); try { final int index = Integer.parseInt(value); if (index != cell.getIndex() && -1 != index && (index < listView.getItems() .size() - 1 || -1 != cell.getIndex())) { event.acceptTransferModes(TransferMode.MOVE); } } catch (final NumberFormatException ignored) { } } } }; } private EventHandler<DragEvent> createDragDroppedHandler(final ListCell<Upload> cell, final ListView<Upload> listView) { return new EventHandler<DragEvent>() { @Override public void handle(final DragEvent event) { final Dragboard db = event.getDragboard(); int myIndex = cell.getIndex(); if (0 > myIndex || myIndex >= listView.getItems().size()) { myIndex = listView.getItems().size() - 1; } final int incomingIndex = Integer.parseInt(db.getString()); listView.getItems().add(myIndex, listView.getItems().remove(incomingIndex)); listView.getSelectionModel().select(myIndex); updateUploadOrder(incomingIndex, myIndex); event.setDropCompleted(true); } void updateUploadOrder(final int from, final int to) { int index_from = from; int index_to = to; if (from > to) { index_from = to; index_to = from; } final List<Upload> items = listView.getItems().subList(index_from, index_to + 1); int i = index_to; for (final Upload upload : items) { upload.setOrder(i--); uploadService.update(upload); } } }; } public class QueueUploadCell extends ListCell<Upload> { private Upload upload; private Parent progressNode; @Override protected void updateItem(final Upload item, final boolean empty) { super.updateItem(item, empty); if (isEmpty()) { if (null != upload) { eventBus.unregister(this); upload = null; } return; } else if (null == upload) { eventBus.register(this); setOnDragDetected(createDragDetectedHandler(this)); setOnDragOver(createDragOverHandler(this, getListView())); setOnDragDropped(createDragDroppedHandler(this, getListView())); } upload = item; final Status status = upload.getStatus(); final Button btnRemove = ButtonBuilder.create() .styleClass("queueCellRemoveButton") .disable(status.isRunning()) .onAction(new QueueCellRemoveButtonHandler(item)) .build(); final Button btnEdit = ButtonBuilder.create() .styleClass("queueCellEditButton") .disable(status.isArchived()) .onAction(new QueueCellEditButtonHandler(item)) .build(); final Button btnAbort = ButtonBuilder.create() .text(resources.getString("button.abort")) .styleClass("queueCellAbortButton") .disable(!status.isRunning() || status.isArchived()) .onAction(new QueueCellAbortButtonHandler(item)) .build(); final ToggleButton btnPauseOnFinish = ToggleButtonBuilder.create() .styleClass("queueCellPauseButton") .onAction(new QueueCellPauseButtonHandler(item)) .selected(item.isPauseOnFinish()) .tooltip(TooltipBuilder.create() .autoHide(true) .text(resources.getString("tooltip.queuecellpause")) .build()) .build(); if (status.isArchived()) { progressNode = HyperlinkBuilder.create() .id("queue-text-" + item.getId()) .styleClass("queueCellHyperlink") .text("http://youtu.be/" + item.getVideoid()) .prefWidth(500) .onAction(new QueueCellHyperlinkHandler(item)) .build(); } else if (status.isFailed()) { //TODO Adjust message? final String statusMessage = "Failed"; // resources.getString(item.getStatus().toLowerCase(Locale.getDefault())); progressNode = LabelBuilder.create() .text(statusMessage) .styleClass("queueCellFailedLabel") .prefWidth(500) .build(); } else if (status.isAborted()) { progressNode = LabelBuilder.create() .text("Aborted") .styleClass("queueCellFailedLabel") .prefWidth(500) .build(); } else { progressNode = progressNodeProvider.get(); } final Label uploadTitle = LabelBuilder.create() .text(extendedPlaceholders.replaceFileTag(upload.getMetadata().getTitle(), upload.getFile())) .styleClass("queueCellTitleLabel") .prefWidth(500) .build(); final String dateFormat = resources.getString("queuecell.date.format"); final String startText = String.format(resources.getString("queuecell.label.started"), getDateString(upload.getDateTimeOfStart(), dateFormat)); final String releaseText = String.format(resources.getString("queuecell.label.released"), getDateString(upload .getDateTimeOfRelease(), dateFormat)); final Label uploadStarted = LabelBuilder.create() .text(startText) .styleClass("queueCellStartedLabel") .prefWidth(300) .build(); final Label uploadReleased = LabelBuilder.create() .text(releaseText) .styleClass("queueCellReleaseLabel") .prefWidth(300) .build(); final HBox containerTop = HBoxBuilder.create() .spacing(5) .styleClass("queueCellTopContainer") .children(uploadTitle, uploadStarted, btnRemove, btnEdit, btnPauseOnFinish) .build(); final HBox containerBottom = HBoxBuilder.create() .spacing(5) .styleClass("queueCellBottomContainer") .children(progressNode, uploadReleased, btnAbort) .build(); final VBox containerPane = VBoxBuilder.create() .children(containerTop, containerBottom) .styleClass("queueCellContainer") .spacing(5) .padding(new Insets(5)) .build(); setGraphic(containerPane); } private String getDateString(final DateTime dateTime, final String dateFormat) { return null == dateTime ? resources.getString("queuecell.label.not_set") : dateTime.toString(dateFormat); } @Subscribe public void onUploadProgress(final UploadJobProgressEvent event) { if (!event.getUpload().equals(upload)) { return; } if (progressNode instanceof ProgressNodeRenderer) { final long speed = event.getDiffBytes() / (event.getDiffTime() + 1) * 1000 + 1; final ProgressNodeRenderer renderer = (ProgressNodeRenderer) progressNode; renderer.setProgress((double) event.getTotalBytesUploaded() / (double) event.getFileSize()); renderer.setEta(calculateEta(event.getFileSize() - event.getTotalBytesUploaded(), speed)); renderer.setSpeed(humanReadableByteCount(speed) + "/s"); renderer.setFinish(calculateFinish(event.getFileSize() - event.getTotalBytesUploaded(), speed)); renderer.setBytes(humanReadableByteCount(event.getTotalBytesUploaded()) + " / " + humanReadableByteCount(event .getFileSize())); } } private String calculateFinish(final long remainingBytes, final long speed) { final long duration = 1000 * remainingBytes / speed + System.currentTimeMillis() + TimeZone.getDefault() .getOffset(System.currentTimeMillis()); return String.format("%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(duration) - TimeUnit.DAYS .toHours(TimeUnit.MILLISECONDS.toDays(duration)), TimeUnit.MILLISECONDS .toMinutes(duration) - TimeUnit.HOURS .toMinutes(TimeUnit.MILLISECONDS.toHours(duration)), TimeUnit.MILLISECONDS .toSeconds(duration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration))); } private String calculateEta(final long remainingBytes, final long speed) { final long duration = 1000 * remainingBytes / speed; return String.format("%d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(duration), TimeUnit.MILLISECONDS .toMinutes(duration) - TimeUnit.HOURS .toMinutes(TimeUnit.MILLISECONDS.toHours(duration)), TimeUnit.MILLISECONDS .toSeconds(duration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration))); } private String humanReadableByteCount(final long bytes) { final int unit = 1024; if (unit > bytes) { return bytes + " Byte"; } final int exp = (int) (Math.log(bytes) / Math.log(unit)); final String pre = String.valueOf("kMGTPE".charAt(exp - 1)); return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } private class QueueCellRemoveButtonHandler implements EventHandler<ActionEvent> { private final Upload item; public QueueCellRemoveButtonHandler(final Upload item) { this.item = item; } @Override public void handle(final ActionEvent event) { if (item.getStatus().isRunning()) { return; } try { final GuiceFXMLLoader.Result result = fxmlLoader.load(getClass().getResource("/de/chaosfisch/uploader/view/ConfirmDialog.fxml"), resources); final ConfirmDialogController controller = result.getController(); controller.setTitle(resources.getString("dialog.removeupload.title")); controller.setMessage(resources.getString("dialog.removeupload.message")); final Parent parent = result.getRoot(); final Scene scene = SceneBuilder.create().root(parent).build(); final Stage stage = StageBuilder.create().scene(scene).build(); stage.initStyle(StageStyle.UNDECORATED); stage.initModality(Modality.APPLICATION_MODAL); stage.showAndWait(); stage.requestFocus(); if (controller.ask()) { uploadService.delete(upload); } } catch (final IOException e) { logger.error("Couldn't load ConfirmDialog", e); } } } private class QueueCellEditButtonHandler implements EventHandler<ActionEvent> { private final Upload item; public QueueCellEditButtonHandler(final Upload item) { this.item = item; } @Override public void handle(final ActionEvent event) { uploadController.fromUpload(item); } } private class QueueCellAbortButtonHandler implements EventHandler<ActionEvent> { private final Upload item; public QueueCellAbortButtonHandler(final Upload item) { this.item = item; } @Override public void handle(final ActionEvent arg0) { if (!item.getStatus().isRunning()) { return; } try { final GuiceFXMLLoader.Result result = fxmlLoader.load(getClass().getResource("/de/chaosfisch/uploader/view/ConfirmDialog.fxml"), resources); final ConfirmDialogController controller = result.getController(); controller.setTitle(resources.getString("dialog.abortupload.title")); controller.setMessage(resources.getString("dialog.abortupload.message")); final Parent parent = result.getRoot(); final Scene scene = SceneBuilder.create().root(parent).build(); final Stage stage = StageBuilder.create().scene(scene).build(); stage.initStyle(StageStyle.UNDECORATED); stage.initModality(Modality.APPLICATION_MODAL); stage.showAndWait(); stage.requestFocus(); if (controller.ask()) { uploadService.abort(item); } } catch (final IOException e) { logger.error("Couldn't load ConfirmDialog", e); } } } private class QueueCellPauseButtonHandler implements EventHandler<ActionEvent> { private final Upload item; public QueueCellPauseButtonHandler(final Upload item) { this.item = item; } @Override public void handle(final ActionEvent event) { final ToggleButton source = (ToggleButton) event.getSource(); item.setPauseOnFinish(source.isSelected()); uploadService.update(item); } } private class QueueCellHyperlinkHandler implements EventHandler<ActionEvent> { private final Upload item; public QueueCellHyperlinkHandler(final Upload item) { this.item = item; } @Override public void handle(final ActionEvent event) { final String url = "http://youtu.be/" + item.getVideoid(); if (!desktopUtil.openBrowser(url)) { dialogHelper.showErrorDialog(resources.getString("dialog.browser_unsupported.title"), String.format(resources .getString("dialog.browser_unsupported.text"), url)); } } } } }