package ctbrec.ui.tabs; import ctbrec.Config; import ctbrec.Model; import ctbrec.event.EventBusHolder; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.sites.mfc.MyFreeCamsClient; import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.ui.*; import ctbrec.ui.action.OpenRecordingsDir; import ctbrec.ui.controls.FasterVerticalScrollPaneSkin; import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchPopover; import ctbrec.ui.controls.SearchPopoverTreeList; import javafx.animation.*; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.concurrent.Worker.State; import javafx.concurrent.WorkerStateEvent; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.*; import javafx.scene.image.ImageView; import javafx.scene.input.*; import javafx.scene.layout.*; import javafx.scene.transform.Transform; import javafx.util.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.SocketTimeoutException; import java.text.DecimalFormat; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import static ctbrec.ui.controls.Dialogs.showError; public class ThumbOverviewTab extends Tab implements TabSelectionListener { private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class); protected static BlockingQueue queue = new LinkedBlockingQueue<>(); static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue, createThreadFactory()); protected FlowPane grid = new FlowPane(); protected PaginatedScheduledService updateService; protected HBox pagination; protected List selectedThumbCells = Collections.synchronizedList(new ArrayList<>()); List filteredThumbCells = Collections.synchronizedList(new ArrayList<>()); Recorder recorder; private String filter; ReentrantLock gridLock = new ReentrantLock(); ScrollPane scrollPane = new ScrollPane(); TextField pageInput = new TextField(Integer.toString(1)); Button pageFirst = new Button("1"); Button pagePrev = new Button("◀"); Button pageNext = new Button("▶"); private volatile boolean updatesSuspended = false; ContextMenu popup; Site site; StackPane root = new StackPane(); Task> searchTask; SearchPopover popover; SearchPopoverTreeList popoverTreeList = new SearchPopoverTreeList(); double imageAspectRatio = 3.0 / 4.0; private final SimpleBooleanProperty preserveAspectRatio = new SimpleBooleanProperty(true); private ComboBox thumbWidth; public ThumbOverviewTab(String title, PaginatedScheduledService updateService, Site site) { super(title); this.updateService = updateService; this.site = site; setClosable(false); createGui(); initializeUpdateService(); } protected void createGui() { grid.setPadding(new Insets(5)); grid.setHgap(5); grid.setVgap(5); SearchBox filterInput = new SearchBox(false); filterInput.setPromptText("Filter models on this page"); filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { filter = filterInput.getText(); gridLock.lock(); try { filter(); moveActiveRecordingsToFront(); } finally { gridLock.unlock(); } }); Tooltip filterTooltip = 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 for public rooms or by resolution.\n\n" + "Try \"1080\" or \">720\" or \"public\""); filterInput.setTooltip(filterTooltip); filterInput.getStyleClass().remove("search-box-icon"); SearchBox searchInput = new SearchBox(); searchInput.setPromptText("Search Model"); searchInput.prefWidth(200); searchInput.textProperty().addListener(search()); searchInput.addEventHandler(KeyEvent.KEY_PRESSED, evt -> { if(evt.getCode() == KeyCode.ESCAPE) { popover.hide(); } }); popover = new SearchPopover(); popover.maxWidthProperty().bind(popover.minWidthProperty()); popover.prefWidthProperty().bind(popover.minWidthProperty()); popover.setMinWidth(400); popover.maxHeightProperty().bind(popover.minHeightProperty()); popover.prefHeightProperty().bind(popover.minHeightProperty()); popover.setMinHeight(450); popover.pushPage(popoverTreeList); StackPane.setAlignment(popover, Pos.TOP_RIGHT); StackPane.setMargin(popover, new Insets(35, 50, 0, 0)); HBox topBar = new HBox(5); HBox.setHgrow(filterInput, Priority.ALWAYS); topBar.getChildren().add(filterInput); if (site.supportsTips() && site.credentialsAvailable()) { Button buyTokens = new Button("Buy Tokens"); buyTokens.setOnAction(e -> DesktopIntegration.open(site.getBuyTokensLink())); TokenLabel tokenBalance = new TokenLabel(site); tokenBalance.setAlignment(Pos.CENTER_RIGHT); tokenBalance.prefHeightProperty().bind(buyTokens.heightProperty()); topBar.getChildren().addAll(tokenBalance, buyTokens); tokenBalance.loadBalance(); } if(site.supportsSearch()) { topBar.getChildren().add(searchInput); } BorderPane.setMargin(topBar, new Insets(0, 5, 0, 5)); scrollPane.setContent(grid); scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); FasterVerticalScrollPaneSkin scrollPaneSkin = new FasterVerticalScrollPaneSkin(scrollPane); scrollPane.setSkin(scrollPaneSkin); BorderPane.setMargin(scrollPane, new Insets(5)); pagination = new HBox(5); pagination.getChildren().add(pageFirst); pagination.getChildren().add(pagePrev); pagination.getChildren().add(pageNext); pagination.getChildren().add(pageInput); BorderPane.setMargin(pagination, new Insets(5)); pageInput.setPrefWidth(50); pageInput.setOnAction(this::handlePageNumberInput); pageFirst.setTooltip(new Tooltip("First Page")); pageFirst.setOnAction(e -> changePageTo(1)); pagePrev.setTooltip(new Tooltip("Previous Page")); pagePrev.setOnAction(e -> previousPage()); pageNext.setTooltip(new Tooltip("Next Page")); pageNext.setOnAction(e -> nextPage()); HBox thumbSizeSelector = new HBox(5); Label l = new Label("Thumb Size"); l.setPadding(new Insets(5,0,0,0)); thumbSizeSelector.getChildren().add(l); List thumbWidths = new ArrayList<>(); thumbWidths.add(180); thumbWidths.add(200); thumbWidths.add(220); thumbWidths.add(270); thumbWidths.add(360); thumbWidth = new ComboBox<>(FXCollections.observableList(thumbWidths)); thumbWidth.getSelectionModel().select(Integer.valueOf(Config.getInstance().getSettings().thumbWidth)); thumbWidth.setOnAction(e -> { Config.getInstance().getSettings().thumbWidth = thumbWidth.getSelectionModel().getSelectedItem(); updateThumbSize(); }); thumbSizeSelector.getChildren().add(thumbWidth); BorderPane.setMargin(thumbSizeSelector, new Insets(5)); BorderPane bottomPane = new BorderPane(); bottomPane.setLeft(pagination); bottomPane.setRight(thumbSizeSelector); BorderPane borderPane = new BorderPane(); borderPane.setPadding(new Insets(5)); borderPane.setTop(topBar); borderPane.setCenter(scrollPane); borderPane.setBottom(bottomPane); root.getChildren().add(borderPane); root.getChildren().add(popover); setContent(root); scrollPane.setOnKeyReleased(event -> { if (event.getCode() == KeyCode.RIGHT) { nextPage(); } else if (event.getCode() == KeyCode.LEFT) { previousPage(); } else if (event.getCode().getCode() >= KeyCode.DIGIT1.getCode() && event.getCode().getCode() <= KeyCode.DIGIT9.getCode()) { changePageTo(event.getCode().getCode() - 48); } }); } private void nextPage() { int page = updateService.getPage(); page++; changePageTo(page); } private void previousPage() { int page = updateService.getPage(); page = Math.max(1, --page); changePageTo(page); } private void changePageTo(int page) { pageInput.setText(Integer.toString(page)); updateService.setPage(page); restartUpdateService(); } private ChangeListener search() { return (observableValue, oldValue, newValue) -> { if(searchTask != null) { searchTask.cancel(true); } if(newValue.length() < 2) { return; } searchTask = new ThumbOverviewTabSearchTask(site, popover, popoverTreeList, newValue); new Thread(searchTask).start(); }; } private void updateThumbSize() { int width = Config.getInstance().getSettings().thumbWidth; thumbWidth.getSelectionModel().select(Integer.valueOf(width)); for (Node node : grid.getChildren()) { if(node instanceof ThumbCell) { ThumbCell cell = (ThumbCell) node; cell.setThumbWidth(width); } } for (ThumbCell cell : filteredThumbCells) { cell.setThumbWidth(width); } } private void handlePageNumberInput(ActionEvent event) { try { int page = Integer.parseInt(pageInput.getText()); page = Math.max(1, page); changePageTo(page); } catch(NumberFormatException e) { // noop } finally { pageInput.setText(Integer.toString(updateService.getPage())); } } private void restartUpdateService() { gridLock.lock(); try { grid.getChildren().clear(); filteredThumbCells.clear(); deselected(); selected(); } finally { gridLock.unlock(); } } void initializeUpdateService() { int refreshRate = Config.getInstance().getSettings().overviewUpdateIntervalInSecs; updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(refreshRate))); updateService.setOnSucceeded(event -> onSuccess()); updateService.setOnFailed(this::onFail); } protected void onSuccess() { if(updatesSuspended) { return; } List models = filterIgnoredModels(updateService.getValue()); updateGrid(models); } private List filterIgnoredModels(List models) { List ignored = Config.getInstance().getSettings().modelsIgnored; return models.stream() .filter(m -> !ignored.contains(m)) .collect(Collectors.toList()); } protected void updateGrid(List models) { gridLock.lock(); try { ObservableList nodes = grid.getChildren(); // first remove models, which are not in the updated list removeModelsMissingInUpdate(nodes, models); // now update existing cells and create new ones models, which are new in the update createOrUpdateModelCells(nodes, models); // reapply the filter filteredThumbCells.clear(); filter(); // move models, which are tracked by the recorder to the front moveActiveRecordingsToFront(); } finally { gridLock.unlock(); } } private void createOrUpdateModelCells(ObservableList nodes, List models) { List positionChangedOrNew = new ArrayList<>(); int index = 0; for (Model model : models) { boolean found = false; for (Node node : nodes) { // NOSONAR 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); } break; } } if(!found) { ThumbCell newCell = createThumbCell(model, recorder); newCell.setIndex(index); positionChangedOrNew.add(newCell); } index++; } rearrangeCells(nodes, positionChangedOrNew); } private void rearrangeCells(ObservableList nodes, List positionChangedOrNew) { for (ThumbCell thumbCell : positionChangedOrNew) { nodes.remove(thumbCell); if(thumbCell.getIndex() < nodes.size()) { nodes.add(thumbCell.getIndex(), thumbCell); } else { nodes.add(thumbCell); } } } private void removeModelsMissingInUpdate(ObservableList nodes, List models) { 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(); } } } ThumbCell createThumbCell(Model model, Recorder recorder) { ThumbCell newCell = new ThumbCell(this, model, recorder, imageAspectRatio); newCell.setImageAspectRatio(imageAspectRatio); newCell.preserveAspectRatioProperty().bind(preserveAspectRatio); newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { suspendUpdates(true); popup = createContextMenu(newCell); popup.show(newCell, event.getScreenX(), event.getScreenY()); popup.setOnHidden(e -> suspendUpdates(false)); event.consume(); }); newCell.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { if(popup != null) { popup.hide(); popup = null; } }); newCell.selectionProperty().addListener((obs, oldValue, newValue) -> { if (Boolean.TRUE.equals(newValue)) { selectedThumbCells.add(newCell); } else { selectedThumbCells.remove(newCell); } }); newCell.setOnMouseClicked(mouseClickListener); return newCell; } private ContextMenu createContextMenu(ThumbCell cell) { MenuItem openInPlayer = new MenuItem("Open in Player"); openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell))); MenuItem start = new MenuItem("Start Recording"); start.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), true)); MenuItem stop = new MenuItem("Stop Recording"); stop.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), false)); MenuItem startStop = recorder.isTracked(cell.getModel()) ? stop : start; MenuItem pause = new MenuItem("Pause Recording"); pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true)); MenuItem resume = new MenuItem("Resume Recording"); resume.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), false)); MenuItem pauseResume = recorder.isSuspended(cell.getModel()) ? resume : pause; MenuItem follow = new MenuItem("Follow"); follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true)); MenuItem unfollow = new MenuItem("Unfollow"); unfollow.setOnAction(e -> follow(getSelectedThumbCells(cell), false)); MenuItem ignore = new MenuItem("Ignore"); ignore.setOnAction(e -> ignore(getSelectedThumbCells(cell))); MenuItem refresh = new MenuItem("Refresh"); refresh.setOnAction(e -> refresh()); MenuItem openRecDir = new MenuItem("Open recording directory"); openRecDir.setOnAction(e -> new OpenRecordingsDir(cell, cell.getModel()).execute()); MenuItem copyUrl = createCopyUrlMenuItem(cell); MenuItem sendTip = createTipMenuItem(cell); configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip); ContextMenu contextMenu = new ContextMenu(); contextMenu.setAutoHide(true); contextMenu.setHideOnEscape(true); contextMenu.setAutoFix(true); contextMenu.getItems().addAll(openInPlayer, startStop); if(recorder.isTracked(cell.getModel())) { contextMenu.getItems().add(pauseResume); } if(site.supportsFollow()) { MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow; followOrUnFollow.setDisable(!site.credentialsAvailable()); contextMenu.getItems().add(followOrUnFollow); } if(site.supportsTips()) { contextMenu.getItems().add(sendTip); } contextMenu.getItems().addAll(copyUrl, ignore, refresh, openRecDir); if(cell.getModel() instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) { MenuItem debug = new MenuItem("debug"); debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(cell.getModel())); contextMenu.getItems().add(debug); } return contextMenu; } private void refresh() { if (updateService.isRunning()) { updateService.cancel(); updateService.reset(); updateService.restart(); } } /* check, if other cells are selected, too. in that case, we have to disable menu items, which make sense only for * single selections. but only do that, if the popup has been triggered on a selected cell. otherwise remove the * selection and show the normal menu */ private void configureItemsForSelection(ThumbCell cell, MenuItem openInPlayer, MenuItem copyUrl, MenuItem sendTip) { if (selectedThumbCells.size() > 1 || selectedThumbCells.size() == 1 && selectedThumbCells.get(0) != cell) { if(cell.isSelected()) { if(Config.getInstance().getSettings().singlePlayer) { openInPlayer.setDisable(true); } copyUrl.setDisable(true); sendTip.setDisable(true); } else { removeSelection(); } } } private MenuItem createCopyUrlMenuItem(ThumbCell cell) { MenuItem copyUrl = new MenuItem("Copy URL"); copyUrl.setOnAction(e -> { final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(cell.getModel().getUrl()); clipboard.setContent(content); }); return copyUrl; } private MenuItem createTipMenuItem(ThumbCell cell) { MenuItem sendTip = new MenuItem("Send Tip"); sendTip.setOnAction(e -> { TipDialog tipDialog = new TipDialog(getTabPane().getScene(), site, cell.getModel()); tipDialog.showAndWait(); String tipText = tipDialog.getResult(); if(tipText != null) { DecimalFormat df = new DecimalFormat("0.##"); try { Number tokens = df.parse(tipText); SiteUiFactory.getUi(site).login(); cell.getModel().receiveTip(tokens.doubleValue()); Map event = new HashMap<>(); event.put("event", "tokens.sent"); event.put("amount", tokens.doubleValue()); EventBusHolder.BUS.post(event); } catch (IOException ex) { LOG.error("An error occurred while sending tip", ex); showError(getTabPane().getScene(), "Couldn't send tip", "An error occurred while sending tip:", ex); } catch (Exception ex) { showError(getTabPane().getScene(), "Couldn't send tip", "You entered an invalid amount of tokens", ex); } } }); sendTip.setDisable(!site.credentialsAvailable()); return sendTip; } private List getSelectedThumbCells(ThumbCell cell) { if(selectedThumbCells.isEmpty()) { return Collections.singletonList(cell); } else { return selectedThumbCells; } } protected void follow(List selection, boolean follow) { for (ThumbCell thumbCell : selection) { thumbCell.follow(follow).thenAccept(success -> { if (follow && Boolean.TRUE.equals(success)) { showAddToFollowedAnimation(thumbCell); } }); } if(!follow) { selectedThumbCells.clear(); } } protected void ignore(List selection) { for (ThumbCell thumbCell : selection) { Model model = thumbCell.getModel(); Config.getInstance().getSettings().modelsIgnored.add(model); grid.getChildren().remove(thumbCell); } selectedThumbCells.clear(); } private void showAddToFollowedAnimation(ThumbCell thumbCell) { Platform.runLater(() -> { Transform tx = thumbCell.getLocalToParentTransform(); ImageView iv = new ImageView(); iv.setFitWidth(thumbCell.getWidth()); root.getChildren().add(iv); StackPane.setAlignment(iv, Pos.TOP_LEFT); iv.setImage(thumbCell.getImage()); double scrollPaneTopLeft = scrollPane.getVvalue() * (grid.getHeight() - scrollPane.getViewportBounds().getHeight()); double offsetInViewPort = tx.getTy() - scrollPaneTopLeft; int duration = 500; TranslateTransition translate = new TranslateTransition(Duration.millis(duration), iv); translate.setFromX(0); translate.setFromY(0); translate.setByX(-tx.getTx() - 200); TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider(); Tab followedTab = tabProvider.getFollowedTab(); translate.setByY(-offsetInViewPort + getFollowedTabYPosition(followedTab)); StackPane.setMargin(iv, new Insets(offsetInViewPort, 0, 0, tx.getTx())); translate.setInterpolator(Interpolator.EASE_BOTH); FadeTransition fade = new FadeTransition(Duration.millis(duration), iv); fade.setFromValue(1); fade.setToValue(.3); ScaleTransition scale = new ScaleTransition(Duration.millis(duration), iv); scale.setToX(0.1); scale.setToY(0.1); ParallelTransition pt = new ParallelTransition(translate, scale); pt.play(); pt.setOnFinished(evt -> root.getChildren().remove(iv)); FollowTabBlinkTransition blink = new FollowTabBlinkTransition(followedTab); blink.play(); }); } private double getFollowedTabYPosition(Tab followedTab) { TabPane tabPane = getTabPane(); int idx = Math.max(0, tabPane.getTabs().indexOf(followedTab)); for (Node node : tabPane.getChildrenUnmodifiable()) { Parent p = (Parent) node; for (Node child : p.getChildrenUnmodifiable()) { if(child.getStyleClass().contains("headers-region")) { Parent tabContainer = (Parent) child; Node tab = tabContainer.getChildrenUnmodifiable().get(tabContainer.getChildrenUnmodifiable().size() - idx - 1); return tab.getLayoutX() - 85; } } } return 0; } private void startStopAction(List selection, boolean start) { for (ThumbCell thumbCell : selection) { thumbCell.startStopAction(start); } } private void pauseResumeAction(List selection, boolean pause) { for (ThumbCell thumbCell : selection) { thumbCell.pauseResumeAction(pause); } } private void startPlayer(List selection) { for (ThumbCell thumbCell : selection) { thumbCell.startPlayer(); } } private final EventHandler mouseClickListener = e -> { ThumbCell cell = (ThumbCell) e.getSource(); if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) { cell.setSelected(false); cell.startPlayer(); } else if (e.getButton() == MouseButton.PRIMARY && e.isControlDown()) { if (popup == null) { cell.setSelected(!cell.isSelected()); } } else if (e.getButton() == MouseButton.PRIMARY) { removeSelection(); } }; protected void onFail(WorkerStateEvent event) { if(updatesSuspended) { return; } Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene()); alert.setTitle("Error"); alert.setHeaderText("Couldn't fetch model list"); if(event.getSource().getException() != null) { if(event.getSource().getException() instanceof SocketTimeoutException) { LOG.debug("Fetching model list timed out"); return; } else { alert.setContentText(event.getSource().getException().getLocalizedMessage()); } LOG.error("Couldn't update model list", event.getSource().getException()); } else { alert.setContentText(event.getEventType().toString()); } alert.showAndWait(); } void filter() { filteredThumbCells.sort((c1, c2) -> { 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); } filteredThumbCells.clear(); 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); cell.setSelected(false); } } // 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.isTracked(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) { try { String[] tokens = filter.split(" "); boolean tokensMissing = false; for (String token : tokens) { if (!modelPropertiesMatchToken(token, m)) { tokensMissing = true; } } return !tokensMissing; } catch (NumberFormatException | ExecutionException | IOException e) { LOG.error("Error while filtering model list", e); return false; } } private boolean modelPropertiesMatchToken(String token, Model m) throws IOException, ExecutionException { int[] resolution = Optional.ofNullable(ThumbCell.resolutionCache.getIfPresent(m)).orElse(new int[2]); String searchText = createSearchText(m); boolean tokensMissing = false; if (token.matches(">\\d+")) { int res = Integer.parseInt(token.substring(1)); if (resolution[1] < res) { tokensMissing = true; } } else if (token.matches("<\\d+")) { int res = Integer.parseInt(token.substring(1)); if (resolution[1] > res) { tokensMissing = true; } } else if (token.equals("public")) { if (m.getOnlineState(true) != ctbrec.Model.State.ONLINE) { tokensMissing = true; } } else { boolean negated = false; if(token.startsWith("!")) { negated = true; token = token.substring(1); } boolean tokenFound = searchText.toLowerCase().contains(token.toLowerCase()); tokensMissing = !tokenFound && !negated || tokenFound && negated; } return !tokensMissing; } private String createSearchText(Model m) { StringBuilder searchTextBuilder = new StringBuilder(m.getName()); searchTextBuilder.append(' '); searchTextBuilder.append(m.getDisplayName()); searchTextBuilder.append(' '); for (String tag : m.getTags()) { searchTextBuilder.append(tag).append(' '); } int[] resolution = Optional.ofNullable(ThumbCell.resolutionCache.getIfPresent(m)).orElse(new int[2]); searchTextBuilder.append(resolution[1]); searchTextBuilder.append(' '); searchTextBuilder.append(Optional.ofNullable(m.getDescription()).orElse("")); return searchTextBuilder.toString().trim(); } public void setRecorder(Recorder recorder) { this.recorder = recorder; popoverTreeList.setRecorder(recorder); } @Override public void selected() { queue.clear(); if(updateService != null) { State s = updateService.getState(); if (s != State.SCHEDULED && s != State.RUNNING) { updateService.reset(); updateService.restart(); } } updateThumbSize(); } @Override public void deselected() { if (updateService != null) { updateService.cancel(); } queue.clear(); for (Iterator iterator = grid.getChildren().iterator(); iterator.hasNext();) { Node node = iterator.next(); if(node instanceof ThumbCell) { ThumbCell thumbCell = (ThumbCell) node; thumbCell.releaseResources(); iterator.remove(); } } } void suspendUpdates(boolean suspend) { this.updatesSuspended = suspend; } private void removeSelection() { while (!selectedThumbCells.isEmpty()) { selectedThumbCells.get(0).setSelected(false); } } private static int threadCounter = 0; private static ThreadFactory createThreadFactory() { return r -> { Thread t = new Thread(r); t.setDaemon(true); t.setPriority(Thread.MIN_PRIORITY); t.setName("ResolutionDetector-" + threadCounter++); return t; }; } public void setImageAspectRatio(double imageAspectRatio) { this.imageAspectRatio = imageAspectRatio; } public BooleanProperty preserveAspectRatioProperty() { return preserveAspectRatio; } }