package ctbrec.ui; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.HttpClient; import ctbrec.Model; import ctbrec.ModelParser; import ctbrec.recorder.Recorder; import javafx.collections.ObservableList; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.concurrent.Worker.State; import javafx.concurrent.WorkerStateEvent; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.util.Duration; import okhttp3.Request; import okhttp3.Response; public class ThumbOverviewTab extends Tab implements TabSelectionListener { private static final transient Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class); static Set resolutionProcessing = Collections.synchronizedSet(new HashSet<>()); static BlockingQueue queue = new LinkedBlockingQueue<>(); static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue); ScheduledService> updateService; Recorder recorder; List filteredThumbCells = Collections.synchronizedList(new ArrayList<>()); String filter; FlowPane grid = new FlowPane(); ReentrantLock gridLock = new ReentrantLock(); ScrollPane scrollPane = new ScrollPane(); String url; boolean loginRequired; HttpClient client = HttpClient.getInstance(); int page = 1; TextField pageInput = new TextField(Integer.toString(page)); Button pagePrev = new Button("◀"); Button pageNext = new Button("▶"); private volatile boolean updatesSuspended = false; public ThumbOverviewTab(String title, String url, boolean loginRequired) { super(title); this.url = url; this.loginRequired = loginRequired; setClosable(false); createGui(); initializeUpdateService(); } private void createGui() { grid.setPadding(new Insets(5)); grid.setHgap(5); grid.setVgap(5); TextField search = new TextField(); search.setPromptText("Filter"); search.textProperty().addListener( (observableValue, oldValue, newValue) -> { filter = search.getText(); gridLock.lock(); try { filter(); } finally { gridLock.unlock(); } }); search.setTooltip(new Tooltip("Filter the models by their name, stream description or #hashtags.\n\n"+"" + "If the display of stream resolution is enabled, you can even filter by resolution. Try \"1080\" or \">720\"")); BorderPane.setMargin(search, new Insets(5)); scrollPane.setContent(grid); scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); BorderPane.setMargin(scrollPane, new Insets(5)); HBox pagination = new HBox(5); pagination.getChildren().add(pagePrev); pagination.getChildren().add(pageNext); pagination.getChildren().add(pageInput); BorderPane.setMargin(pagination, new Insets(5)); pageInput.setPrefWidth(50); pageInput.setOnAction((e) -> handlePageNumberInput()); pagePrev.setOnAction((e) -> { page = Math.max(1, --page); pageInput.setText(Integer.toString(page)); restartUpdateService(); }); pageNext.setOnAction((e) -> { page++; pageInput.setText(Integer.toString(page)); restartUpdateService(); }); BorderPane root = new BorderPane(); root.setPadding(new Insets(5)); root.setTop(search); root.setCenter(scrollPane); root.setBottom(pagination); setContent(root); } private void handlePageNumberInput() { try { page = Integer.parseInt(pageInput.getText()); page = Math.max(1, page); restartUpdateService(); } catch(NumberFormatException e) { } finally { pageInput.setText(Integer.toString(page)); } } private void restartUpdateService() { gridLock.lock(); try { grid.getChildren().clear(); filteredThumbCells.clear(); deselected(); selected(); } finally { gridLock.unlock(); } } void initializeUpdateService() { updateService = createUpdateService(); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); updateService.setOnSucceeded((event) -> onSuccess()); updateService.setOnFailed((event) -> onFail(event)); } protected void onSuccess() { if(updatesSuspended) { return; } gridLock.lock(); try { List models = updateService.getValue(); ObservableList nodes = grid.getChildren(); // first remove models, which are not in the updated list for (Iterator iterator = nodes.iterator(); iterator.hasNext();) { Node node = iterator.next(); if (!(node instanceof ThumbCell)) continue; ThumbCell cell = (ThumbCell) node; if(!models.contains(cell.getModel())) { iterator.remove(); } } List positionChangedOrNew = new ArrayList<>(); int index = 0; for (Model model : models) { boolean found = false; for (Iterator iterator = nodes.iterator(); iterator.hasNext();) { Node node = iterator.next(); if (!(node instanceof ThumbCell)) continue; ThumbCell cell = (ThumbCell) node; if(cell.getModel().equals(model)) { found = true; cell.setModel(model); if(index != cell.getIndex()) { cell.setIndex(index); positionChangedOrNew.add(cell); } } } if(!found) { ThumbCell newCell = new ThumbCell(this, model, recorder, client); newCell.setIndex(index); positionChangedOrNew.add(newCell); } index++; } for (ThumbCell thumbCell : positionChangedOrNew) { nodes.remove(thumbCell); if(thumbCell.getIndex() < nodes.size()) { nodes.add(thumbCell.getIndex(), thumbCell); } else { nodes.add(thumbCell); } } filter(); moveActiveRecordingsToFront(); } finally { gridLock.unlock(); } } protected void onFail(WorkerStateEvent event) { if(updatesSuspended) { return; } Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Couldn't fetch model list"); if(event.getSource().getException() != null) { alert.setContentText(event.getSource().getException().getLocalizedMessage()); } else { alert.setContentText(event.getEventType().toString()); } alert.showAndWait(); } private void filter() { Collections.sort(filteredThumbCells, new Comparator() { @Override public int compare(Node o1, Node o2) { ThumbCell c1 = (ThumbCell) o1; ThumbCell c2 = (ThumbCell) o2; if(c1.getIndex() < c2.getIndex()) return -1; if(c1.getIndex() > c2.getIndex()) return 1; return c1.getModel().getName().compareTo(c2.getModel().getName()); } }); if (filter == null || filter.isEmpty()) { for (ThumbCell thumbCell : filteredThumbCells) { insert(thumbCell); } return; } // remove the ones from grid, which don't match for (Iterator iterator = grid.getChildren().iterator(); iterator.hasNext();) { Node node = iterator.next(); ThumbCell cell = (ThumbCell) node; Model m = cell.getModel(); if(!matches(m, filter)) { iterator.remove(); filteredThumbCells.add(cell); } } // add the ones, which might have been filtered before, but now match for (Iterator iterator = filteredThumbCells.iterator(); iterator.hasNext();) { ThumbCell thumbCell = iterator.next(); Model m = thumbCell.getModel(); if(matches(m, filter)) { iterator.remove(); insert(thumbCell); } } } private void moveActiveRecordingsToFront() { List thumbsToMove = new ArrayList<>(); ObservableList thumbs = grid.getChildren(); for (int i = thumbs.size()-1; i > 0; i--) { ThumbCell thumb = (ThumbCell) thumbs.get(i); if(recorder.isRecording(thumb.getModel())) { thumbs.remove(i); thumbsToMove.add(0, thumb); } } thumbs.addAll(0, thumbsToMove); } private void insert(ThumbCell thumbCell) { if(grid.getChildren().contains(thumbCell)) { return; } if(thumbCell.getIndex() < grid.getChildren().size()-1) { grid.getChildren().add(thumbCell.getIndex(), thumbCell); } else { grid.getChildren().add(thumbCell); } } private boolean matches(Model m, String filter) { String[] tokens = filter.split(" "); StringBuilder searchTextBuilder = new StringBuilder(m.getName()); searchTextBuilder.append(' '); for (String tag : m.getTags()) { searchTextBuilder.append(tag).append(' '); } searchTextBuilder.append(m.getStreamResolution()); String searchText = searchTextBuilder.toString().trim(); //LOG.debug("{} -> {}", m.getName(), searchText); boolean tokensMissing = false; for (String token : tokens) { if(token.matches(">\\d+")) { int res = Integer.parseInt(token.substring(1)); if(m.getStreamResolution() < res) { tokensMissing = true; } } else if(token.matches("<\\d+")) { int res = Integer.parseInt(token.substring(1)); if(m.getStreamResolution() > res) { tokensMissing = true; } } else if(!searchText.contains(token)) { tokensMissing = true; } } return !tokensMissing; } private ScheduledService> createUpdateService() { ScheduledService> updateService = new ScheduledService>() { @Override protected Task> createTask() { return new Task>() { @Override public List call() throws IOException { String url = ThumbOverviewTab.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis(); LOG.debug("Fetching page {}", url); Request request = new Request.Builder().url(url).build(); Response response = client.execute(request, loginRequired); if (response.isSuccessful()) { List models = ModelParser.parseModels(response.body().string()); response.close(); return models; } else { int code = response.code(); response.close(); throw new IOException("HTTP status " + code); } } }; } }; ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); t.setName("ThumbOverviewTab UpdateService"); return t; } }); updateService.setExecutor(executor); return updateService; } public void setRecorder(Recorder recorder) { this.recorder = recorder; } @Override public void selected() { if(updateService != null) { State s = updateService.getState(); if (s != State.SCHEDULED && s != State.RUNNING) { updateService.reset(); updateService.restart(); } } } @Override public void deselected() { if(updateService != null) { updateService.cancel(); } } void suspendUpdates(boolean suspend) { this.updatesSuspended = suspend; } }