/* * Copyright (c) 2014-2016 Jan Strauß <jan[at]over9000.eu> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package eu.over9000.skadi.ui.dialogs; import de.jensd.fx.glyphs.GlyphsDude; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; import eu.over9000.cathode.data.RootBox; import eu.over9000.skadi.model.ChannelStore; import eu.over9000.skadi.service.CheckAuthService; import eu.over9000.skadi.service.PutFollowedService; import eu.over9000.skadi.service.RetrieveFollowedService; import eu.over9000.skadi.ui.StatusBarWrapper; import eu.over9000.skadi.ui.cells.SyncListCell; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; public class SyncDialog extends Dialog<Boolean> { private final StatusBarWrapper sb; private final ChannelStore channelStore; private final ObservableList<String> localOnStart = FXCollections.observableArrayList(); private final ObservableList<String> remoteOnStart = FXCollections.observableArrayList(); private final ObservableList<String> localWorking = FXCollections.observableArrayList(); private final ObservableList<String> remoteWorking = FXCollections.observableArrayList(); private final ListView<String> localChannelView = new ListView<>(new SortedList<>(localWorking, String::compareTo)); private final ListView<String> remoteChannelView = new ListView<>(new SortedList<>(remoteWorking, String::compareTo)); private final BorderPane localPane = new BorderPane(new ProgressIndicator()); private final BorderPane remotePane = new BorderPane(new ProgressIndicator()); private GridPane contentPane; private String user; private VBox buttonBar; private Button btSelectedToRemote; private Button btSelectedToLocal; private boolean localChanges = false; public SyncDialog(final ChannelStore channelStore, final StatusBarWrapper sb) { this.channelStore = channelStore; this.sb = sb; setTitle("Followed Channels synchronization"); setHeaderText(null); setGraphic(null); localChannelView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); remoteChannelView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); BorderPane.setMargin(localChannelView, new Insets(5, 0, 5, 0)); BorderPane.setMargin(remoteChannelView, new Insets(5, 0, 5, 0)); localPane.setTop(new Label("Skadi followed channels")); remotePane.setTop(new Label("Twitch followed channels")); localPane.setBottom(genListButtonBar(localWorking, localOnStart, remoteChannelView)); remotePane.setBottom(genListButtonBar(remoteWorking, remoteOnStart, localChannelView)); localPane.setMinSize(250, 600); remotePane.setMinSize(250, 600); getDialogPane().setContent(getContentPane()); localChannelView.setCellFactory(param -> new SyncListCell(remoteWorking)); remoteChannelView.setCellFactory(param -> new SyncListCell(localWorking)); localChannelView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { btSelectedToRemote.setDisable(newValue == null); }); remoteChannelView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { btSelectedToLocal.setDisable(newValue == null); }); final ButtonType close = new ButtonType("Apply", ButtonBar.ButtonData.OK_DONE); getDialogPane().getButtonTypes().addAll(close, ButtonType.CANCEL); setupAuthService(); setupLocalList(); setResultConverter(param -> localChanges); final Button btOk = (Button) getDialogPane().lookupButton(close); btOk.addEventFilter(ActionEvent.ACTION, event -> { final List<String> diffLocal = localWorking.stream().filter(c -> !localOnStart.contains(c)).collect(Collectors.toList()); final List<String> diffRemote = remoteWorking.stream().filter(c -> !remoteOnStart.contains(c)).collect(Collectors.toList()); final Alert confirmChanges = new Alert(Alert.AlertType.CONFIRMATION, "Add " + diffLocal.size() + " channels to Skadi's list and add " + diffRemote.size() + " to your twitch follows?"); final Optional<ButtonType> result = confirmChanges.showAndWait(); if (result.isPresent() && result.get() == ButtonType.OK) { performSync(diffLocal, diffRemote); } else { event.consume(); } }); } private void performSync(final List<String> diffLocal, final List<String> diffRemote) { // local if (!diffLocal.isEmpty()) { channelStore.addChannels(diffLocal, sb); localChanges = true; } //remote if (!diffRemote.isEmpty()) { final PutFollowedService service = new PutFollowedService(user, diffRemote, sb); service.start(); } } private HBox genListButtonBar(final ObservableList<String> working, final ObservableList<String> onStart, final ListView<String> otherView) { final Button btReset = new Button("Reset"); btReset.setOnAction(event -> { working.clear(); working.addAll(onStart); forceListRefreshOn(otherView); }); final HBox result = new HBox(5, btReset); result.setAlignment(Pos.CENTER_RIGHT); return result; } private void setupLocalList() { localOnStart.addAll(channelStore.getChannelNames()); localWorking.addAll(localOnStart); localPane.setCenter(localChannelView); } private void setupRemoteList(final Set<String> channels) { remoteOnStart.addAll(channels); remoteWorking.addAll(remoteOnStart); remotePane.setCenter(remoteChannelView); forceListRefreshOn(localChannelView); } private void setupAuthService() { final CheckAuthService authService = new CheckAuthService(); authService.setOnSucceeded(event -> { final RootBox result = (RootBox) event.getSource().getValue(); if (result.getToken().isValid()) { user = result.getToken().getUserName(); setupRemoteService(); } else { sb.updateStatusText("failed to retrieve username, stopping"); close(); } }); authService.setOnFailed(event -> { sb.updateStatusText("failed to retrieve username, stopping"); close(); }); authService.start(); } private void setupRemoteService() { final RetrieveFollowedService followedService = new RetrieveFollowedService(user); followedService.setOnSucceeded(event -> { final Set<String> result = followedService.getValue(); setupRemoteList(result); }); followedService.setOnFailed(event -> { sb.updateStatusText("failed to retrieve followed channels from twitch, stopping"); close(); }); followedService.start(); } public GridPane getContentPane() { if (contentPane == null) { contentPane = new GridPane(); contentPane.setHgap(10); contentPane.setVgap(10); getContentPane().add(localPane, 0, 0); getContentPane().add(getButtonBar(), 1, 0); getContentPane().add(remotePane, 2, 0); } return contentPane; } public VBox getButtonBar() { if (buttonBar == null) { btSelectedToRemote = GlyphsDude.createIconButton(FontAwesomeIcon.ANGLE_RIGHT); btSelectedToLocal = GlyphsDude.createIconButton(FontAwesomeIcon.ANGLE_LEFT); final Button btAllToRemote = GlyphsDude.createIconButton(FontAwesomeIcon.ANGLE_DOUBLE_RIGHT); final Button btAllToLocal = GlyphsDude.createIconButton(FontAwesomeIcon.ANGLE_DOUBLE_LEFT); btSelectedToRemote.setDisable(true); btSelectedToLocal.setDisable(true); btSelectedToRemote.setOnAction(event -> { final ObservableList<String> selected = localChannelView.getSelectionModel().getSelectedItems(); selected.stream().filter(c -> !remoteWorking.contains(c)).forEach(remoteWorking::add); forceListRefreshOn(localChannelView); }); btSelectedToLocal.setOnAction(event -> { final ObservableList<String> selected = remoteChannelView.getSelectionModel().getSelectedItems(); selected.stream().filter(c -> !localWorking.contains(c)).forEach(localWorking::add); forceListRefreshOn(remoteChannelView); }); btAllToRemote.setOnAction(event -> { localWorking.stream().filter(c -> !remoteWorking.contains(c)).forEach(remoteWorking::add); forceListRefreshOn(localChannelView); }); btAllToLocal.setOnAction(event -> { remoteWorking.stream().filter(c -> !localWorking.contains(c)).forEach(localWorking::add); forceListRefreshOn(remoteChannelView); }); btSelectedToRemote.setMinSize(32, 32); btSelectedToLocal.setMinSize(32, 32); btAllToRemote.setMinSize(32, 32); btAllToLocal.setMinSize(32, 32); buttonBar = new VBox(10, btSelectedToRemote, btSelectedToLocal, btAllToRemote, btAllToLocal); buttonBar.setAlignment(Pos.CENTER); } return buttonBar; } private <T> void forceListRefreshOn(final ListView<T> listView) { final ObservableList<T> items = listView.<T>getItems(); listView.<T>setItems(null); listView.<T>setItems(items); } }