diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java
index 537eed29..d9814e87 100644
--- a/client/src/main/java/ctbrec/ui/CamrecApplication.java
+++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java
@@ -123,6 +123,8 @@ public class CamrecApplication extends Application {
         switchToStartTab();
 
         primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css");
+        primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css");
+        primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css");
         primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue());
         primaryStage.getScene().heightProperty()
         .addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java
index 5874f8c2..63366614 100644
--- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java
+++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java
@@ -24,7 +24,7 @@ import ctbrec.Model;
 import ctbrec.Recording;
 import ctbrec.recorder.Recorder;
 import ctbrec.sites.Site;
-import ctbrec.ui.autofilltextbox.AutoFillTextField;
+import ctbrec.ui.controls.AutoFillTextField;
 import javafx.application.Platform;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java
index 8b563344..52a39f89 100644
--- a/client/src/main/java/ctbrec/ui/ThumbCell.java
+++ b/client/src/main/java/ctbrec/ui/ThumbCell.java
@@ -181,6 +181,8 @@ public class ThumbCell extends StackPane {
         if(Config.getInstance().getSettings().determineResolution) {
             determineResolution();
         }
+
+        update();
     }
 
     public void setSelected(boolean selected) {
@@ -478,7 +480,7 @@ public class ThumbCell extends StackPane {
         setRecording(recorder.isRecording(model));
         setImage(model.getPreview());
         String txt = recording ? "    " : "";
-        txt += model.getDescription();
+        txt += model.getDescription() != null ? model.getDescription() : "";
         topic.setText(txt);
 
         if(Config.getInstance().getSettings().determineResolution) {
diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java
index 0aad00b2..5fb861e4 100644
--- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java
+++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java
@@ -30,6 +30,9 @@ import ctbrec.recorder.Recorder;
 import ctbrec.sites.Site;
 import ctbrec.sites.mfc.MyFreeCamsClient;
 import ctbrec.sites.mfc.MyFreeCamsModel;
+import ctbrec.ui.controls.SearchBox;
+import ctbrec.ui.controls.SearchPopover;
+import ctbrec.ui.controls.SearchPopoverTreeList;
 import javafx.animation.FadeTransition;
 import javafx.animation.Interpolator;
 import javafx.animation.ParallelTransition;
@@ -37,8 +40,10 @@ import javafx.animation.ScaleTransition;
 import javafx.animation.Transition;
 import javafx.animation.TranslateTransition;
 import javafx.application.Platform;
+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.EventHandler;
@@ -61,11 +66,14 @@ import javafx.scene.image.ImageView;
 import javafx.scene.input.Clipboard;
 import javafx.scene.input.ClipboardContent;
 import javafx.scene.input.ContextMenuEvent;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseButton;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.BorderPane;
 import javafx.scene.layout.FlowPane;
 import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
 import javafx.scene.layout.StackPane;
 import javafx.scene.paint.Color;
 import javafx.scene.transform.Transform;
@@ -96,6 +104,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
     ContextMenu popup;
     Site site;
     StackPane root = new StackPane();
+    Task<List<Model>> searchTask;
+    SearchPopover popover;
+    SearchPopoverTreeList popoverTreelist = new SearchPopoverTreeList();
 
     private ComboBox<Integer> thumbWidth;
 
@@ -113,10 +124,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
         grid.setHgap(5);
         grid.setVgap(5);
 
-        TextField search = new TextField();
-        search.setPromptText("Filter models on this page");
-        search.textProperty().addListener( (observableValue, oldValue, newValue) -> {
-            filter = search.getText();
+        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();
@@ -125,12 +136,41 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
                 gridLock.unlock();
             }
         });
-        Tooltip searchTooltip = new Tooltip("Filter the models by their name, stream description or #hashtags.\n\n"
+        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\"");
-        search.setTooltip(searchTooltip);
+        filterInput.setTooltip(filterTooltip);
+        filterInput.getStyleClass().remove("search-box-icon");
+        BorderPane.setMargin(filterInput, new Insets(5));
 
-        BorderPane.setMargin(search, new Insets(5));
+        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();
+            }
+        });
+        BorderPane.setMargin(searchInput, new Insets(5));
+
+        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(400);
+        popover.pushPage(popoverTreelist);
+        StackPane.setAlignment(popover, Pos.TOP_RIGHT);
+        StackPane.setMargin(popover, new Insets(50, 50, 0, 0));
+
+        HBox topBar = new HBox(5);
+        HBox.setHgrow(filterInput, Priority.ALWAYS);
+        topBar.getChildren().add(filterInput);
+        if(site.supportsSearch()) {
+            topBar.getChildren().add(searchInput);
+        }
 
         scrollPane.setContent(grid);
         scrollPane.setFitToHeight(true);
@@ -186,14 +226,69 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
 
         BorderPane borderPane = new BorderPane();
         borderPane.setPadding(new Insets(5));
-        borderPane.setTop(search);
+        borderPane.setTop(topBar);
         borderPane.setCenter(scrollPane);
         borderPane.setBottom(bottomPane);
 
         root.getChildren().add(borderPane);
+        root.getChildren().add(popover);
         setContent(root);
     }
 
+    private ChangeListener<? super String> search() {
+        return (observableValue, oldValue, newValue) -> {
+            if(searchTask != null) {
+                searchTask.cancel(true);
+            }
+
+            if(newValue.length() < 2) {
+                return;
+            }
+
+
+            searchTask = new Task<List<Model>>() {
+                @Override
+                protected List<Model> call() throws Exception {
+                    if(site.searchRequiresLogin()) {
+                        boolean loggedin = false;
+                        try {
+                            loggedin = SiteUiFactory.getUi(site).login();
+                        } catch (IOException e) {
+                            loggedin = false;
+                        }
+                        if(!loggedin) {
+                            showError("Login failed", "Search won't work correctly without login", null);
+                        }
+                    }
+                    return site.search(newValue);
+                }
+
+                @Override
+                protected void failed() {
+                    LOG.error("Search failed", getException());
+                }
+
+                @Override
+                protected void succeeded() {
+                    Platform.runLater(() -> {
+                        List<Model> models = getValue();
+                        LOG.debug("Search result {} {}", isCancelled(), models);
+                        if(models.isEmpty()) {
+                            popover.hide();
+                        } else {
+                            popoverTreelist.getItems().clear();
+                            for (Model model : getValue()) {
+                                popoverTreelist.getItems().add(model);
+                            }
+                            popover.show();
+                        }
+                    });
+                }
+            };
+            new Thread(searchTask).start();
+        };
+    }
+
     private void updateThumbSize() {
         int width = Config.getInstance().getSettings().thumbWidth;
         thumbWidth.getSelectionModel().select(Integer.valueOf(width));
@@ -375,18 +470,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
                         event.put("amount", tokens);
                         CamrecApplication.bus.post(event);
                     } catch (Exception e1) {
-                        Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
-                        alert.setTitle("Error");
-                        alert.setHeaderText("Couldn't send tip");
-                        alert.setContentText("An error occured while sending tip: " + e1.getLocalizedMessage());
-                        alert.showAndWait();
+                        showError("Couldn't send tip", "An error occured while sending tip:", e1);
                     }
                 } else {
-                    Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
-                    alert.setTitle("Error");
-                    alert.setHeaderText("Couldn't send tip");
-                    alert.setContentText("You entered an invalid amount of tokens");
-                    alert.showAndWait();
+                    showError("Couldn't send tip", "You entered an invalid amount of tokens", null);
                 }
             }
         });
@@ -700,6 +787,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
 
     public void setRecorder(Recorder recorder) {
         this.recorder = recorder;
+        popoverTreelist.setRecorder(recorder);
     }
 
     @Override
@@ -731,4 +819,24 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
             selectedThumbCells.get(0).setSelected(false);
         }
     }
+
+    private void showError(String header, String text, Exception e) {
+        Runnable r = () -> {
+            Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
+            alert.setTitle("Error");
+            alert.setHeaderText(header);
+            String content = text;
+            if(e != null) {
+                content += " " + e.getLocalizedMessage();
+            }
+            alert.setContentText(content);
+            alert.showAndWait();
+        };
+
+        if(Platform.isFxApplicationThread()) {
+            r.run();
+        } else {
+            Platform.runLater(r);
+        }
+    }
 }
diff --git a/client/src/main/java/ctbrec/ui/autofilltextbox/AutoFillTextField.java b/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java
similarity index 98%
rename from client/src/main/java/ctbrec/ui/autofilltextbox/AutoFillTextField.java
rename to client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java
index bf986360..ca772778 100644
--- a/client/src/main/java/ctbrec/ui/autofilltextbox/AutoFillTextField.java
+++ b/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java
@@ -1,4 +1,4 @@
-package ctbrec.ui.autofilltextbox;
+package ctbrec.ui.controls;
 
 
 import javafx.collections.ObservableList;
diff --git a/client/src/main/java/ctbrec/ui/controls/Popover.css b/client/src/main/java/ctbrec/ui/controls/Popover.css
new file mode 100644
index 00000000..7fc3e632
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/Popover.css
@@ -0,0 +1,74 @@
+.popover {
+    -fx-padding: 43 7 7 7;
+}
+.popover-frame {
+    -fx-border-image-source: url("/popover-empty.png");
+    -fx-border-image-slice: 78 50 60 120 fill;
+    -fx-border-image-width: 78 50 60 120;
+    -fx-border-image-insets: -32 -37 -47 -37;
+}
+.popover.right-tooth .popover-frame {
+    -fx-border-image-slice: 78 120 60 50 fill;
+    -fx-border-image-width: 78 120 60 50;
+}
+.popover-title {
+    /*-fx-font-family: "Bree serif"; */
+    -fx-font-family: "Source Sans Pro Light";
+    -fx-font-size: 20px;
+ /*   -fx-text-fill: white;
+    -fx-font-weight: bold; */
+}
+.popover .button {
+    -fx-font-family: "Source Sans Pro";
+    -fx-font-size: 12px;
+}
+
+.popover-tree-list-cell {
+    -fx-background-color: white;
+   /*  -fx-border-color: transparent transparent #dfdfdf transparent; */
+    -fx-padding: 0 30 0 12;
+    /*-fx-font-family: "Bree Serif"; */
+    -fx-font-size: 15px;
+ /*   -fx-font-weight: bold; */
+    -fx-text-fill: #363636;
+}
+#PopoverBackground {
+    -fx-background-color: white;
+}
+.search-result-cell {
+    -fx-background-color: white;
+    -fx-padding: 4 30 4 45;
+}
+.search-result-cell:selected {
+  /*   -fx-background-color: white, #eeeeee; */
+    -fx-background-insets: 0, 0 0 0 40;
+}
+.search-result-cell .title {
+    /*-fx-font-family: "Bree Serif"; */
+    -fx-font-size: 15px;
+  /*  -fx-font-weight: bold; */
+    -fx-text-fill: #363636;
+}
+.search-result-cell .details {
+    -fx-font-size: 13px;
+    -fx-text-fill: #444444;
+}
+.search-icon-pane .label {
+    -fx-font-family: "Source Sans Pro Semibold";
+    -fx-font-size: 16px;
+    -fx-background-color: #515151;
+    -fx-background-radius: 3px;
+    -fx-text-fill: white;
+    -fx-alignment: center;
+}
+.sample-tree-list-cell {
+    -fx-background-color: white;
+    -fx-border-color: transparent transparent #dfdfdf transparent;
+    -fx-padding: 0 30 0 20;
+    -fx-font-size: 15px;
+    -fx-text-fill: #363636;
+    -fx-graphic-text-gap: 20px;
+}
+#PopoverBackground {
+    -fx-background-color: white;
+}
\ No newline at end of file
diff --git a/client/src/main/java/ctbrec/ui/controls/Popover.java b/client/src/main/java/ctbrec/ui/controls/Popover.java
new file mode 100644
index 00000000..a6bec1f2
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/Popover.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (c) 2008, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package ctbrec.ui.controls;
+
+import java.util.LinkedList;
+
+import javafx.animation.Animation;
+import javafx.animation.FadeTransition;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.ParallelTransition;
+import javafx.animation.ScaleTransition;
+import javafx.animation.Timeline;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Point2D;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Text;
+import javafx.util.Duration;
+
+/**
+ * A Popover is a mini-window that pops up and contains some application specific content.
+ * It's width is defined by the application, but defaults to a hard-coded pref width.
+ * The height will always be between a minimum height (determined by the application, but
+ * pre-set with a minimum value) and a maximum height (specified by the application, or
+ * based on the height of the scene). The value for the pref height is determined by
+ * inspecting the pref height of the current displayed page. At time this value is animated
+ * (when switching from page to page).
+ */
+public class Popover extends Region implements EventHandler<Event>{
+    private static final int PAGE_GAP = 15;
+
+    /**
+     * The visual frame of the popover is defined as an addition region, rather than simply styling
+     * the popover itself as one might expect. The reason for this is that our frame is styled via
+     * a border image, and it has an inner shadow associated with it, and we want to be able to ensure
+     * that the shadow is on top of whatever content is drawn within the popover. In addition, the inner
+     * edge of the frame is rounded, and we want the content to slide under it, only to be clipped beneath
+     * the frame. So it works best for the frame to be overlaid on top, even though it is not intuitive.
+     */
+    private final Region frameBorder = new Region();
+    private final Button leftButton = new Button("Left");
+    private final Button rightButton = new Button("Right");
+    private final LinkedList<Page> pages = new LinkedList<Page>();
+    private final Pane pagesPane = new Pane();
+    private final Rectangle pagesClipRect = new Rectangle();
+    private final Pane titlesPane = new Pane();
+    private Text title; // the current title
+    private final Rectangle titlesClipRect = new Rectangle();
+    //    private final EventHandler<ScrollEvent> popoverScrollHandler;
+    private final EventHandler<MouseEvent> popoverHideHandler;
+    private Runnable onHideCallback = null;
+    private int maxPopupHeight = -1;
+
+    private DoubleProperty popoverHeight = new SimpleDoubleProperty(400) {
+        @Override protected void invalidated() {
+            requestLayout();
+        }
+    };
+
+    public Popover() {
+        // TODO Could pagesPane be a region instead? I need to draw some opaque background. Right now when
+        // TODO animating from one page to another you can see the background "shine through" because the
+        // TODO group background is transparent. That can't be good for performance either.
+        getStyleClass().setAll("popover");
+        frameBorder.getStyleClass().setAll("popover-frame");
+        frameBorder.setMouseTransparent(true);
+        // setup buttons
+        leftButton.setOnMouseClicked(this);
+        leftButton.getStyleClass().add("popover-left-button");
+        leftButton.setMinWidth(USE_PREF_SIZE);
+        rightButton.setOnMouseClicked(this);
+        rightButton.getStyleClass().add("popover-right-button");
+        rightButton.setMinWidth(USE_PREF_SIZE);
+        pagesClipRect.setSmooth(false);
+        pagesPane.setClip(pagesClipRect);
+        titlesClipRect.setSmooth(false);
+        titlesPane.setClip(titlesClipRect);
+        getChildren().addAll(pagesPane, frameBorder, titlesPane, leftButton, rightButton);
+        // always hide to start with
+        setVisible(false);
+        setOpacity(0);
+        setScaleX(.8);
+        setScaleY(.8);
+        // create handlers for auto hiding
+        popoverHideHandler = (MouseEvent t) -> {
+            // check if event is outside popup
+            Point2D mouseInFilterPane = sceneToLocal(t.getX(), t.getY());
+            if (mouseInFilterPane.getX() < 0 || mouseInFilterPane.getX() > (getWidth()) ||
+                    mouseInFilterPane.getY() < 0 || mouseInFilterPane.getY() > (getHeight())) {
+                hide();
+                t.consume();
+            }
+        };
+        //        popoverScrollHandler = new EventHandler<ScrollEvent>() {
+        //            @Override public void handle(ScrollEvent t) {
+        //                t.consume(); // consume all scroll events
+        //            }
+        //        };
+    }
+
+    /**
+     * Handle mouse clicks on the left and right buttons.
+     */
+    @Override public void handle(Event event) {
+        if (event.getSource() == leftButton) {
+            pages.getFirst().handleLeftButton();
+        } else if (event.getSource() == rightButton) {
+            pages.getFirst().handleRightButton();
+        }
+    }
+
+    @Override protected double computeMinWidth(double height) {
+        Page page = pages.isEmpty() ? null : pages.getFirst();
+        if (page != null) {
+            Node n = page.getPageNode();
+            if (n != null) {
+                Insets insets = getInsets();
+                return insets.getLeft() + n.minWidth(-1) + insets.getRight();
+            }
+        }
+        return 200;
+    }
+
+    @Override protected double computeMinHeight(double width) {
+        Insets insets = getInsets();
+        return insets.getLeft() + 100 + insets.getRight();
+    }
+
+    @Override protected double computePrefWidth(double height) {
+        Page page = pages.isEmpty() ? null : pages.getFirst();
+        if (page != null) {
+            Node n = page.getPageNode();
+            if (n != null) {
+                Insets insets = getInsets();
+                return insets.getLeft() + n.prefWidth(-1) + insets.getRight();
+            }
+        }
+        return 400;
+    }
+
+    @Override protected double computePrefHeight(double width) {
+        double minHeight = minHeight(-1);
+        double maxHeight = maxHeight(-1);
+        double prefHeight = popoverHeight.get();
+        if (prefHeight == -1) {
+            Page page = pages.getFirst();
+            if (page != null) {
+                Insets inset = getInsets();
+                if (width == -1) {
+                    width = prefWidth(-1);
+                }
+                double contentWidth = width - inset.getLeft() - inset.getRight();
+                double contentHeight = page.getPageNode().prefHeight(contentWidth);
+                prefHeight = inset.getTop() + contentHeight + inset.getBottom();
+                popoverHeight.set(prefHeight);
+            } else {
+                prefHeight = minHeight;
+            }
+        }
+        return boundedSize(minHeight, prefHeight, maxHeight);
+    }
+
+    static double boundedSize(double min, double pref, double max) {
+        double a = pref >= min ? pref : min;
+        double b = min >= max ? min : max;
+        return a <= b ? a : b;
+    }
+
+    @Override protected double computeMaxWidth(double height) {
+        return Double.MAX_VALUE;
+    }
+
+    @Override protected double computeMaxHeight(double width) {
+        Scene scene = getScene();
+        if (scene != null) {
+            return scene.getHeight() - 100;
+        } else {
+            return Double.MAX_VALUE;
+        }
+    }
+
+    @Override protected void layoutChildren() {
+        if (maxPopupHeight == -1) {
+            maxPopupHeight = (int)getScene().getHeight()-100;
+        }
+        final Insets insets = getInsets();
+        final int width = (int)getWidth();
+        final int height = (int)getHeight();
+        final int top = (int)insets.getTop();
+        final int right = (int)insets.getRight();
+        final int bottom = (int)insets.getBottom();
+        final int left = (int)insets.getLeft();
+
+        int pageWidth = width - left - right;
+        int pageHeight = height - top - bottom;
+
+        frameBorder.resize(width, height);
+
+        pagesPane.resizeRelocate(left, top, pageWidth, pageHeight);
+        pagesClipRect.setWidth(pageWidth);
+        pagesClipRect.setHeight(pageHeight);
+
+        int pageX = 0;
+        for (Node page : pagesPane.getChildren()) {
+            page.resizeRelocate(pageX, 0, pageWidth, pageHeight);
+            pageX += pageWidth + PAGE_GAP;
+        }
+
+        int buttonHeight = (int)(leftButton.prefHeight(-1));
+        if (buttonHeight < 30) buttonHeight = 30;
+        final int buttonTop = (int)((top-buttonHeight)/2d);
+        final int leftButtonWidth = (int)snapSizeX(leftButton.prefWidth(-1));
+        leftButton.resizeRelocate(left, buttonTop,leftButtonWidth,buttonHeight);
+        final int rightButtonWidth = (int)snapSizeX(rightButton.prefWidth(-1));
+        rightButton.resizeRelocate(width-right-rightButtonWidth, buttonTop,rightButtonWidth,buttonHeight);
+
+        final double leftButtonRight = leftButton.isVisible() ? (left + leftButtonWidth) : left;
+        final double rightButtonLeft = rightButton.isVisible() ? (right + rightButtonWidth) : right;
+        titlesClipRect.setX(leftButtonRight);
+        titlesClipRect.setWidth(pageWidth - leftButtonRight - rightButtonLeft);
+        titlesClipRect.setHeight(top);
+
+        if (title != null) {
+            title.setTranslateY((int) (top / 2d));
+        }
+    }
+
+    public final void clearPages() {
+        while (!pages.isEmpty()) {
+            pages.pop().handleHidden();
+        }
+        pagesPane.getChildren().clear();
+        titlesPane.getChildren().clear();
+        pagesClipRect.setX(0);
+        pagesClipRect.setWidth(400);
+        pagesClipRect.setHeight(400);
+        popoverHeight.set(400);
+        pagesPane.setTranslateX(0);
+        titlesPane.setTranslateX(0);
+        titlesClipRect.setTranslateX(0);
+    }
+
+    public final void popPage() {
+        Page oldPage = pages.pop();
+        oldPage.handleHidden();
+        oldPage.setPopover(null);
+        Page page = pages.getFirst();
+        leftButton.setVisible(page.leftButtonText() != null);
+        leftButton.setText(page.leftButtonText());
+        rightButton.setVisible(page.rightButtonText() != null);
+        rightButton.setText(page.rightButtonText());
+        if (pages.size() > 0) {
+            final Insets insets = getInsets();
+            final int width = (int)prefWidth(-1);
+            final int right = (int)insets.getRight();
+            final int left = (int)insets.getLeft();
+            int pageWidth = width - left - right;
+            final int newPageX = (pageWidth+PAGE_GAP) * (pages.size()-1);
+            new Timeline(
+                    new KeyFrame(Duration.millis(350), (ActionEvent t) -> {
+                        pagesPane.setCache(false);
+                        pagesPane.getChildren().remove(pagesPane.getChildren().size()-1);
+                        titlesPane.getChildren().remove(titlesPane.getChildren().size()-1);
+                        resizePopoverToNewPage(pages.getFirst().getPageNode());
+                    },
+                            new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH),
+                            new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH),
+                            new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH),
+                            new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH)
+                            )
+                    ).play();
+        } else {
+            hide();
+        }
+    }
+
+    public final void pushPage(final Page page) {
+        final Node pageNode = page.getPageNode();
+        pageNode.setManaged(false);
+        pagesPane.getChildren().add(pageNode);
+        final Insets insets = getInsets();
+        final int pageWidth = (int)(prefWidth(-1) - insets.getLeft() - insets.getRight());
+        final int newPageX = (pageWidth + PAGE_GAP) * pages.size();
+        leftButton.setVisible(page.leftButtonText() != null);
+        leftButton.setText(page.leftButtonText());
+        rightButton.setVisible(page.rightButtonText() != null);
+        rightButton.setText(page.rightButtonText());
+
+        title = new Text(page.getPageTitle());
+        title.getStyleClass().add("popover-title");
+        //debtest title.setFill(Color.WHITE);
+        title.setTextOrigin(VPos.CENTER);
+        title.setTranslateX(newPageX + (int) ((pageWidth - title.getLayoutBounds().getWidth()) / 2d));
+        titlesPane.getChildren().add(title);
+
+        if (!pages.isEmpty() && isVisible()) {
+            final Timeline timeline = new Timeline(
+                    new KeyFrame(Duration.millis(350), (ActionEvent t) -> {
+                        pagesPane.setCache(false);
+                        resizePopoverToNewPage(pageNode);
+                    },
+                            new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH),
+                            new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH),
+                            new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH),
+                            new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH)
+                            )
+                    );
+            timeline.play();
+        }
+        page.setPopover(this);
+        page.handleShown();
+        pages.push(page);
+    }
+
+    private void resizePopoverToNewPage(final Node newPageNode) {
+        final Insets insets = getInsets();
+        final double width = prefWidth(-1);
+        final double contentWidth = width - insets.getLeft() - insets.getRight();
+        double h = newPageNode.prefHeight(contentWidth);
+        h += insets.getTop() + insets.getBottom();
+        new Timeline(
+                new KeyFrame(Duration.millis(200),
+                        new KeyValue(popoverHeight, h, Interpolator.EASE_BOTH)
+                        )
+                ).play();
+    }
+
+    public void show(){
+        show(null);
+    }
+
+    private Animation fadeAnimation = null;
+
+    public void show(Runnable onHideCallback){
+        if (!isVisible() || fadeAnimation != null) {
+            this.onHideCallback = onHideCallback;
+            getScene().addEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler);
+            //            getScene().addEventFilter(ScrollEvent.ANY,popoverScrollHandler);
+
+            if (fadeAnimation != null) {
+                fadeAnimation.stop();
+                setVisible(true); // for good measure
+            } else {
+                popoverHeight.set(-1);
+                setVisible(true);
+            }
+
+            FadeTransition fade = new FadeTransition(Duration.seconds(.1), this);
+            fade.setToValue(1.0);
+            fade.setOnFinished((ActionEvent event) -> {
+                fadeAnimation = null;
+            });
+
+            ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this);
+            scale.setToX(1);
+            scale.setToY(1);
+
+            ParallelTransition tx = new ParallelTransition(fade, scale);
+            fadeAnimation = tx;
+            tx.play();
+        }
+    }
+
+    public void hide(){
+        if (isVisible() || fadeAnimation != null) {
+            getScene().removeEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler);
+            //            getScene().removeEventFilter(ScrollEvent.ANY,popoverScrollHandler);
+
+            if (fadeAnimation != null) {
+                fadeAnimation.stop();
+            }
+
+            FadeTransition fade = new FadeTransition(Duration.seconds(.1), this);
+            fade.setToValue(0);
+            fade.setOnFinished((ActionEvent event) -> {
+                fadeAnimation = null;
+                setVisible(false);
+                //clearPages();
+                if (onHideCallback != null) onHideCallback.run();
+            });
+
+            ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this);
+            scale.setToX(.8);
+            scale.setToY(.8);
+
+            ParallelTransition tx = new ParallelTransition(fade, scale);
+            fadeAnimation = tx;
+            tx.play();
+        }
+    }
+
+    /**
+     * Represents a page in a popover.
+     */
+    public static interface Page {
+        public void setPopover(Popover popover);
+        public Popover getPopover();
+
+        /**
+         * Get the node that represents the page.
+         *
+         * @return the page node.
+         */
+        public Node getPageNode();
+
+        /**
+         * Get the title to display for this page.
+         *
+         * @return The page title
+         */
+        public String getPageTitle();
+
+        /**
+         * The text for left button, if null then button will be hidden.
+         * @return The button text
+         */
+        public String leftButtonText();
+
+        /**
+         * Called on a click of the left button of the popover.
+         */
+        public void handleLeftButton();
+
+        /**
+         * The text for right button, if null then button will be hidden.
+         * @return The button text
+         */
+        public String rightButtonText();
+
+        /**
+         * Called on a click of the right button of the popover.
+         */
+        public void handleRightButton();
+
+        public void handleShown();
+        public void handleHidden();
+    }
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java
new file mode 100644
index 00000000..01f6aac1
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2008, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package ctbrec.ui.controls;
+
+import javafx.event.EventHandler;
+import javafx.geometry.Bounds;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.util.Callback;
+
+/**
+ * Special ListView designed to look like "Text... &gt;" tree list. Perhaps we ought to have customized
+ * a TreeView instead of a ListView (as the TreeView already has the data model all defined).
+ *
+ * This implementation minimizes classes by just having the PopoverTreeList implementing everything
+ * (it is the Control, the Skin, and the CellFactory all in one).
+ */
+public class PopoverTreeList<T> extends ListView<T> implements Callback<ListView<T>, ListCell<T>> {
+    protected static final Image RIGHT_ARROW = new Image(
+            PopoverTreeList.class.getResource("/popover-arrow.png").toExternalForm());
+
+    public PopoverTreeList(){
+        getStyleClass().clear();
+        setCellFactory(this);
+    }
+
+    @Override public ListCell<T> call(ListView<T> p) {
+        return new TreeItemListCell();
+    }
+
+    protected void itemClicked(T item) {}
+
+    private class TreeItemListCell extends ListCell<T> implements EventHandler<MouseEvent> {
+        private ImageView arrow = new ImageView(RIGHT_ARROW);
+
+        private TreeItemListCell() {
+            super();
+            getStyleClass().setAll("popover-tree-list-cell");
+            setOnMouseClicked(this);
+        }
+
+        @Override public void handle(MouseEvent t) {
+            itemClicked(getItem());
+        }
+
+        @Override protected double computePrefWidth(double height) {
+            return 100;
+        }
+
+        @Override protected double computePrefHeight(double width) {
+            return 44;
+        }
+
+        @Override protected void layoutChildren() {
+            if (getChildren().size() < 2) getChildren().add(arrow);
+            super.layoutChildren();
+            final int w = (int)getWidth();
+            final int h = (int)getHeight();
+            //final int centerX = (int)(w/2d);
+            //final int centerY = (int)(h/2d);
+            final Bounds arrowBounds = arrow.getLayoutBounds();
+            arrow.setLayoutX(w - arrowBounds.getWidth() - 12);
+            arrow.setLayoutY((int)((h - arrowBounds.getHeight())/2d));
+        }
+
+        // CELL METHODS
+        @Override protected void updateItem(T item, boolean empty) {
+            // let super do its work
+            super.updateItem(item,empty);
+            // update our state
+            if (item == null) { // empty item
+                setText(null);
+                arrow.setVisible(false);
+            } else {
+                setText(item.toString());
+                arrow.setVisible(true);
+            }
+        }
+    }
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/SearchBox.css b/client/src/main/java/ctbrec/ui/controls/SearchBox.css
new file mode 100644
index 00000000..1ec1ebd5
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/SearchBox.css
@@ -0,0 +1,34 @@
+.search-box-icon {
+    -fx-shape: "M10.728,9.893c0.889-1.081,1.375-2.435,1.375-3.842C12.103,2.714,9.388,0,6.051,0C2.715,0,0,2.714,0,6.051c0,3.338,2.715,6.052,6.051,6.052c0.954,0,1.898-0.227,2.744-0.656l3.479,3.478l1.743-1.742L10.728,9.893z M6.051,2.484c1.966,0,3.566,1.602,3.566,3.566c0,1.968-1.6,3.567-3.566,3.567c-1.967,0-3.566-1.6-3.566-3.567C2.485,4.086,4.084,2.484,6.051,2.484z";
+    -fx-scale-shape: false;
+    -fx-background-color: #aaaaaa;
+}
+.search-box {
+    /*-fx-font-size: 16px;*/
+    /*-fx-text-fill: #363636;*/
+    /*-fx-background-radius: 15, 14;*/
+    -fx-padding: 0 0 0 30;
+}
+.search-box:focused {
+    /*-fx-background-radius: 15,14,16,14;*/
+}
+.search-clear-button {
+    -fx-shape: "M9.521,0.083c-5.212,0-9.438,4.244-9.438,9.479c0,5.234,4.225,9.479,9.438,9.479c5.212,0,9.437-4.244,9.437-9.479C18.958,4.327,14.733,0.083,9.521,0.083z M13.91,13.981c-0.367,0.369-0.963,0.369-1.329,0l-3.019-3.03l-3.019,3.03c-0.367,0.369-0.962,0.369-1.329,0c-0.367-0.368-0.366-0.965,0.001-1.334l3.018-3.031L5.216,6.585C4.849,6.217,4.849,5.618,5.217,5.25c0.366-0.369,0.961-0.368,1.328,0l3.018,3.031l3.019-3.031c0.366-0.368,0.961-0.369,1.328,0c0.366,0.368,0.366,0.967,0,1.335l-3.019,3.031l3.02,3.031C14.276,13.017,14.276,13.613,13.91,13.981z";
+    -fx-scale-shape: false;
+    -fx-background-color: #aaaaaa;
+    -fx-padding: 9.5px;
+}
+
+.search-tree-list-cell {
+    -fx-background-color: white;
+    -fx-border-color: transparent transparent #dfdfdf transparent;
+    -fx-padding: 0 30 0 20;
+    -fx-font-size: 15px;
+    -fx-text-fill: #363636;
+    -fx-graphic-text-gap: 20px;
+}
+
+.highlight {
+    -fx-background-color: #0096c9;
+    -fx-text-fill: white;
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/SearchBox.java b/client/src/main/java/ctbrec/ui/controls/SearchBox.java
new file mode 100644
index 00000000..be893acd
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/SearchBox.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2008, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package ctbrec.ui.controls;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.Cursor;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+
+/**
+ * Search field with styling and a clear button
+ */
+public class SearchBox extends TextField implements ChangeListener<String>{
+    private final Button clearButton = new Button();
+    private final Region innerBackground = new Region();
+    private final Region icon = new Region();
+    private final int prefHeight = 26;
+
+    public SearchBox() {
+        getStyleClass().addAll("search-box");
+        icon.getStyleClass().setAll("search-box-icon");
+        innerBackground.getStyleClass().setAll("search-box-inner");
+        setPromptText("Search");
+        textProperty().addListener(this);
+        setPrefHeight(prefHeight);
+        clearButton.getStyleClass().setAll("search-clear-button");
+        clearButton.setCursor(Cursor.DEFAULT);
+        clearButton.setOnMouseClicked((MouseEvent t) -> {
+            setText("");
+            requestFocus();
+        });
+        clearButton.setVisible(false);
+        clearButton.setManaged(false);
+        innerBackground.setManaged(false);
+        icon.setManaged(false);
+    }
+
+    public SearchBox(boolean icon) {
+        this();
+        this.icon.setVisible(false);
+        this.icon.getStyleClass().remove("search-box-icon");
+        this.setStyle("-fx-padding: 5");
+    }
+
+    @Override protected void layoutChildren() {
+        super.layoutChildren();
+        if (clearButton.getParent() != this) getChildren().add(clearButton);
+        if (innerBackground.getParent() != this) getChildren().add(0,innerBackground);
+        if (icon.getParent() != this) getChildren().add(icon);
+        innerBackground.setLayoutX(0);
+        innerBackground.setLayoutY(0);
+        innerBackground.resize(getWidth(), getHeight());
+        icon.setLayoutX(0);
+        icon.setLayoutY(0);
+        icon.resize(35,prefHeight);
+        clearButton.setLayoutX(getWidth() - prefHeight);
+        clearButton.setLayoutY(0);
+        clearButton.resize(prefHeight, prefHeight);
+    }
+
+    @Override public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) {
+        clearButton.setVisible(newValue.length() > 0);
+    }
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopover.java b/client/src/main/java/ctbrec/ui/controls/SearchPopover.java
new file mode 100644
index 00000000..42222952
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/SearchPopover.java
@@ -0,0 +1,9 @@
+package ctbrec.ui.controls;
+
+public class SearchPopover extends Popover {
+
+
+    public SearchPopover() {
+        getStyleClass().add("right-tooth");
+    }
+}
diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java
new file mode 100644
index 00000000..722cca72
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2008, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *  - Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *  - Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the distribution.
+ *  - Neither the name of Oracle Corporation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package ctbrec.ui.controls;
+
+import java.util.Optional;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ctbrec.Model;
+import ctbrec.recorder.Recorder;
+import ctbrec.ui.Player;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.Skin;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+
+/**
+ * Popover page that displays a list of samples and sample categories for a given SampleCategory.
+ */
+public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Popover.Page {
+    private static final transient Logger LOG = LoggerFactory.getLogger(SearchPopoverTreeList.class);
+
+    private Popover popover;
+
+    private Recorder recorder;
+
+    public SearchPopoverTreeList() {
+
+    }
+
+    @Override
+    public ListCell<Model> call(ListView<Model> p) {
+        return new SearchItemListCell();
+    }
+
+    @Override
+    protected void itemClicked(Model model) {
+        if(model == null) {
+            return;
+        }
+
+        setCursor(Cursor.WAIT);
+        new Thread(() -> {
+            Platform.runLater(() -> {
+                boolean started = Player.play(model);
+                if(started) {
+                    Toast.makeText(getScene(), "Starting Player", 2000, 500, 500);
+                }
+                setCursor(Cursor.DEFAULT);
+            });
+        }).start();
+    }
+
+    @Override
+    public void setPopover(Popover popover) {
+        this.popover = popover;
+    }
+
+    @Override
+    public Popover getPopover() {
+        return popover;
+    }
+
+    @Override
+    public Node getPageNode() {
+        return this;
+    }
+
+    @Override
+    public String getPageTitle() {
+        return "Search Results";
+    }
+
+    @Override
+    public String leftButtonText() {
+        return null;
+    }
+
+    @Override
+    public void handleLeftButton() {
+    }
+
+    @Override
+    public String rightButtonText() {
+        return "Done";
+    }
+
+    @Override
+    public void handleRightButton() {
+        popover.hide();
+    }
+
+    @Override
+    public void handleShown() {
+    }
+
+    @Override
+    public void handleHidden() {
+    }
+
+    private class SearchItemListCell extends ListCell<Model> implements Skin<SearchItemListCell>, EventHandler<MouseEvent> {
+
+        private Label title = new Label();
+        private Button follow;
+        private Button record;
+        private Model model;
+        private ImageView thumb = new ImageView();
+        private int thumbSize = 64;
+        private Node tallest = thumb;
+
+        private SearchItemListCell() {
+            super();
+            setSkin(this);
+            getStyleClass().setAll("search-tree-list-cell");
+            setOnMouseClicked(this);
+            setOnMouseEntered(evt -> {
+                getStyleClass().add("highlight");
+                title.getStyleClass().add("highlight");
+            });
+            setOnMouseExited(evt -> {
+                getStyleClass().remove("highlight");
+                title.getStyleClass().remove("highlight");
+            });
+            thumb.setFitWidth(thumbSize);
+            thumb.setFitHeight(thumbSize);
+
+            follow = new Button("Follow");
+            follow.setOnAction((evt) -> {
+                setCursor(Cursor.WAIT);
+                new Thread(new Task<Boolean>() {
+                    @Override
+                    protected Boolean call() throws Exception {
+                        return model.follow();
+                    }
+
+                    @Override
+                    protected void done() {
+                        try {
+                            get();
+                        } catch (Exception e) {
+                            LOG.warn("Search failed: {}", e.getMessage());
+                        }
+                        Platform.runLater(() -> setCursor(Cursor.DEFAULT));
+                    }
+                }).start();
+            });
+            record = new Button("Record");
+            record.setOnAction((evt) -> {
+                setCursor(Cursor.WAIT);
+                new Thread(new Task<Void>() {
+                    @Override
+                    protected Void call() throws Exception {
+                        recorder.startRecording(model);
+                        return null;
+                    }
+
+                    @Override
+                    protected void done() {
+                        Platform.runLater(() -> setCursor(Cursor.DEFAULT));
+                    }
+                }).start();
+            });
+            getChildren().addAll(thumb, title, follow, record);
+
+            record.visibleProperty().bind(title.visibleProperty());
+            thumb.visibleProperty().bind(title.visibleProperty());
+        }
+
+        @Override
+        public void handle(MouseEvent t) {
+            itemClicked(getItem());
+        }
+
+        @Override
+        protected void updateItem(Model model, boolean empty) {
+            super.updateItem(model, empty);
+            if (empty) {
+                follow.setVisible(false);
+                title.setVisible(false);
+                this.model = null;
+            } else {
+                follow.setVisible(model.getSite().supportsFollow());
+                title.setVisible(true);
+                title.setText(model.getName());
+                this.model = model;
+                String previewUrl = Optional.ofNullable(model.getPreview()).orElse(getClass().getResource("/anonymous.png").toString());
+                Image img = new Image(previewUrl, true);
+                thumb.setImage(img);
+            }
+
+        }
+
+        @Override
+        protected void layoutChildren() {
+            super.layoutChildren();
+            final Insets insets = getInsets();
+            final double left = insets.getLeft();
+            final double top = insets.getTop();
+            final double w = getWidth() - left - insets.getRight();
+            final double h = getHeight() - top - insets.getBottom();
+
+            thumb.setLayoutX(left);
+            thumb.setLayoutY((h - thumbSize) / 2);
+
+            final double titleHeight = title.prefHeight(w);
+            title.setLayoutX(left + thumbSize + 10);
+            title.setLayoutY((h - titleHeight) / 2);
+            title.resize(w, titleHeight);
+
+            int buttonW = 50;
+            int buttonH = 24;
+            follow.setStyle("-fx-font-size: 10px;");
+            follow.setLayoutX(w - buttonW - 20);
+            follow.setLayoutY((h - buttonH) / 2);
+            follow.resize(buttonW, buttonH);
+
+            record.setStyle("-fx-font-size: 10px;");
+            record.setLayoutX(w - 10);
+            record.setLayoutY((h - buttonH) / 2);
+            record.resize(buttonW, buttonH);
+        }
+
+        @Override
+        protected double computeMinWidth(double height) {
+            final Insets insets = getInsets();
+            final double h = height = insets.getBottom() - insets.getTop();
+            return (int) ((insets.getLeft() + tallest.minWidth(h) + tallest.minWidth(h) + insets.getRight()) + 0.5d);
+        }
+
+        @Override
+        protected double computePrefWidth(double height) {
+            final Insets insets = getInsets();
+            final double h = height = insets.getBottom() - insets.getTop();
+            return (int) ((insets.getLeft() + tallest.prefWidth(h) + tallest.prefWidth(h) + insets.getRight()) + 0.5d);
+        }
+
+        @Override
+        protected double computeMaxWidth(double height) {
+            final Insets insets = getInsets();
+            final double h = height = insets.getBottom() - insets.getTop();
+            return (int) ((insets.getLeft() + tallest.maxWidth(h) + tallest.maxWidth(h) + insets.getRight()) + 0.5d);
+        }
+
+        @Override
+        protected double computeMinHeight(double width) {
+            return thumbSize;
+        }
+
+        @Override
+        protected double computePrefHeight(double width) {
+            return thumbSize + 20;
+        }
+
+        @Override
+        protected double computeMaxHeight(double width) {
+            return thumbSize + 20;
+        }
+
+        @Override
+        public SearchItemListCell getSkinnable() {
+            return this;
+        }
+
+        @Override
+        public Node getNode() {
+            return null;
+        }
+
+        @Override
+        public void dispose() {
+        }
+    }
+
+    public void setRecorder(Recorder recorder) {
+        this.recorder = recorder;
+    }
+}
\ No newline at end of file
diff --git a/client/src/main/resources/anonymous.png b/client/src/main/resources/anonymous.png
new file mode 100644
index 00000000..b0294b26
Binary files /dev/null and b/client/src/main/resources/anonymous.png differ
diff --git a/client/src/main/resources/popover-arrow.png b/client/src/main/resources/popover-arrow.png
new file mode 100644
index 00000000..289e753c
Binary files /dev/null and b/client/src/main/resources/popover-arrow.png differ
diff --git a/client/src/main/resources/popover-arrow@2x.png b/client/src/main/resources/popover-arrow@2x.png
new file mode 100644
index 00000000..bf4d0d17
Binary files /dev/null and b/client/src/main/resources/popover-arrow@2x.png differ
diff --git a/client/src/main/resources/popover-empty.png b/client/src/main/resources/popover-empty.png
new file mode 100644
index 00000000..c5808ec1
Binary files /dev/null and b/client/src/main/resources/popover-empty.png differ
diff --git a/client/src/main/resources/popover-empty@2x.png b/client/src/main/resources/popover-empty@2x.png
new file mode 100644
index 00000000..74149f1f
Binary files /dev/null and b/client/src/main/resources/popover-empty@2x.png differ
diff --git a/common/src/main/java/ctbrec/sites/AbstractSite.java b/common/src/main/java/ctbrec/sites/AbstractSite.java
index 1d50d186..96d67005 100644
--- a/common/src/main/java/ctbrec/sites/AbstractSite.java
+++ b/common/src/main/java/ctbrec/sites/AbstractSite.java
@@ -1,5 +1,10 @@
 package ctbrec.sites;
 
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import ctbrec.Model;
 import ctbrec.recorder.Recorder;
 
 public abstract class AbstractSite implements Site {
@@ -26,4 +31,19 @@ public abstract class AbstractSite implements Site {
     public Recorder getRecorder() {
         return recorder;
     }
+
+    @Override
+    public boolean supportsSearch() {
+        return false;
+    }
+
+    @Override
+    public List<Model> search(String q) throws IOException, InterruptedException {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public boolean searchRequiresLogin() {
+        return false;
+    }
 }
diff --git a/common/src/main/java/ctbrec/sites/Site.java b/common/src/main/java/ctbrec/sites/Site.java
index 08fef0f4..cf6f3119 100644
--- a/common/src/main/java/ctbrec/sites/Site.java
+++ b/common/src/main/java/ctbrec/sites/Site.java
@@ -1,6 +1,7 @@
 package ctbrec.sites;
 
 import java.io.IOException;
+import java.util.List;
 
 import ctbrec.Model;
 import ctbrec.io.HttpClient;
@@ -21,8 +22,11 @@ public interface Site {
     public void shutdown();
     public boolean supportsTips();
     public boolean supportsFollow();
+    public boolean supportsSearch();
     public boolean isSiteForModel(Model m);
     public boolean credentialsAvailable();
     public void setEnabled(boolean enabled);
     public boolean isEnabled();
+    public List<Model> search(String q) throws IOException, InterruptedException;
+    public boolean searchRequiresLogin();
 }
diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java
index f763bccc..6b2670d8 100644
--- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java
+++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java
@@ -1,8 +1,15 @@
 package ctbrec.sites.bonga;
 
 import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
+import org.json.JSONArray;
 import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import ctbrec.Config;
 import ctbrec.Model;
@@ -16,6 +23,8 @@ import okhttp3.Response;
 
 public class BongaCams extends AbstractSite {
 
+    private static final transient Logger LOG = LoggerFactory.getLogger(BongaCams.class);
+
     public static final String BASE_URL = "https://bongacams.com";
 
     private BongaCamsHttpClient httpClient;
@@ -116,6 +125,54 @@ public class BongaCams extends AbstractSite {
         return false;
     }
 
+    @Override
+    public boolean supportsSearch() {
+        return true;
+    }
+
+    @Override
+    public boolean searchRequiresLogin() {
+        return true;
+    }
+
+    @Override
+    public List<Model> search(String q) throws IOException, InterruptedException {
+        String url = BASE_URL + "/tools/listing_v3.php?offset=0&model_search[display_name][text]=" + URLEncoder.encode(q, "utf-8");
+        Request req = new Request.Builder()
+                .url(url)
+                .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
+                .addHeader("Accept", "application/json, text/javascript, */*")
+                .addHeader("Accept-Language", "en")
+                .addHeader("Referer", BongaCams.BASE_URL)
+                .addHeader("X-Requested-With", "XMLHttpRequest")
+                .build();
+        try(Response response = getHttpClient().execute(req)) {
+            if(response.isSuccessful()) {
+                String body = response.body().string();
+                JSONObject json = new JSONObject(body);
+                if(json.optString("status").equals("success")) {
+                    List<Model> models = new ArrayList<>();
+                    JSONArray results = json.getJSONArray("models");
+                    for (int i = 0; i < results.length(); i++) {
+                        JSONObject result = results.getJSONObject(i);
+                        Model model = createModel(result.getString("username"));
+                        String thumb = result.getString("thumb_image");
+                        if(thumb != null) {
+                            model.setPreview("https:" + thumb);
+                        }
+                        models.add(model);
+                    }
+                    return models;
+                } else {
+                    LOG.warn("Search result: " + json.toString(2));
+                    return Collections.emptyList();
+                }
+            } else {
+                throw new HttpException(response.code(), response.message());
+            }
+        }
+    }
+
     @Override
     public boolean isSiteForModel(Model m) {
         return m instanceof BongaCamsModel;
diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4.java b/common/src/main/java/ctbrec/sites/cam4/Cam4.java
index d63ef050..04b032f4 100644
--- a/common/src/main/java/ctbrec/sites/cam4/Cam4.java
+++ b/common/src/main/java/ctbrec/sites/cam4/Cam4.java
@@ -1,16 +1,25 @@
 package ctbrec.sites.cam4;
 
 import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
 
 import ctbrec.Config;
 import ctbrec.Model;
+import ctbrec.StringUtil;
 import ctbrec.io.HttpClient;
+import ctbrec.io.HttpException;
 import ctbrec.sites.AbstractSite;
+import okhttp3.Request;
+import okhttp3.Response;
 
 public class Cam4 extends AbstractSite {
 
     public static final String BASE_URI = "https://www.cam4.com";
-
     public static final String AFFILIATE_LINK = BASE_URI + "/?referrerId=1514a80d87b5effb456cca02f6743aa1";
 
     private HttpClient httpClient;
@@ -84,6 +93,57 @@ public class Cam4 extends AbstractSite {
         return true;
     }
 
+    @Override
+    public boolean supportsSearch() {
+        return true;
+    }
+
+    @Override
+    public boolean searchRequiresLogin() {
+        return true;
+    }
+
+    @Override
+    public List<Model> search(String q) throws IOException, InterruptedException {
+        List<Model> result = new ArrayList<>();
+        search(q, false, result);
+        search(q, true, result);
+        return result;
+    }
+
+    private void search(String q, boolean offline, List<Model> models) throws IOException {
+        String url = BASE_URI + "/usernameSearch?username=" + URLEncoder.encode(q, "utf-8");
+        if(offline) {
+            url += "&offline=true";
+        }
+        Request req = new Request.Builder()
+                .url(url)
+                .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
+                .build();
+        try(Response response = getHttpClient().execute(req)) {
+            if(response.isSuccessful()) {
+                String body = response.body().string();
+                JSONArray results = new JSONArray(body);
+                for (int i = 0; i < results.length(); i++) {
+                    JSONObject result = results.getJSONObject(i);
+                    Model model = createModel(result.getString("username"));
+                    String thumb = null;
+                    if(result.has("thumbnailId")) {
+                        thumb = "https://snapshots.xcdnpro.com/thumbnails/" + model.getName() + "?s=" + result.getString("thumbnailId");
+                    } else {
+                        thumb = result.getString("profileImageLink");
+                    }
+                    if(StringUtil.isNotBlank(thumb)) {
+                        model.setPreview(thumb);
+                    }
+                    models.add(model);
+                }
+            } else {
+                throw new HttpException(response.code(), response.message());
+            }
+        }
+    }
+
     @Override
     public boolean isSiteForModel(Model m) {
         return m instanceof Cam4Model;
diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java
index c8750bc5..e79688fa 100644
--- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java
+++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java
@@ -1,8 +1,15 @@
 package ctbrec.sites.camsoda;
 
 import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
+import org.json.JSONArray;
 import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import ctbrec.Config;
 import ctbrec.Model;
@@ -14,6 +21,7 @@ import okhttp3.Response;
 
 public class Camsoda extends AbstractSite {
 
+    private static final transient Logger LOG = LoggerFactory.getLogger(Camsoda.class);
     public static final String BASE_URI = "https://www.camsoda.com";
     private HttpClient httpClient;
 
@@ -105,6 +113,44 @@ public class Camsoda extends AbstractSite {
         return true;
     }
 
+    @Override
+    public boolean supportsSearch() {
+        return true;
+    }
+
+    @Override
+    public List<Model> search(String q) throws IOException, InterruptedException {
+        String url = BASE_URI + "/api/v1/browse/autocomplete?s=" + URLEncoder.encode(q, "utf-8");
+        Request req = new Request.Builder()
+                .url(url)
+                .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
+                .build();
+        try(Response response = getHttpClient().execute(req)) {
+            if(response.isSuccessful()) {
+                JSONObject json = new JSONObject(response.body().string());
+                if(json.optBoolean("status")) {
+                    List<Model> models = new ArrayList<>();
+                    JSONArray results = json.getJSONArray("results");
+                    for (int i = 0; i < results.length(); i++) {
+                        JSONObject result = results.getJSONObject(i);
+                        CamsodaModel model = (CamsodaModel) createModel(result.getString("username"));
+                        String thumb = result.getString("thumb");
+                        if(thumb != null) {
+                            model.setPreview("https:" + thumb);
+                        }
+                        models.add(model);
+                    }
+                    return models;
+                } else {
+                    LOG.warn("Search result: " + json.toString(2));
+                    return Collections.emptyList();
+                }
+            } else {
+                throw new HttpException(response.code(), response.message());
+            }
+        }
+    }
+
     @Override
     public boolean isSiteForModel(Model m) {
         return m instanceof CamsodaModel;
diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java
index 105be013..36125f85 100644
--- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java
+++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java
@@ -3,6 +3,10 @@ package ctbrec.sites.chaturbate;
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URLEncoder;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -68,6 +72,7 @@ public class Chaturbate extends AbstractSite {
         ChaturbateModel m = new ChaturbateModel(this);
         m.setName(name);
         m.setUrl(getBaseUrl() + '/' + name + '/');
+        m.setPreview("https://roomimg.stream.highwebmedia.com/ri/" + name + ".jpg?" + Instant.now().getEpochSecond());
         return m;
     }
 
@@ -124,6 +129,44 @@ public class Chaturbate extends AbstractSite {
         return true;
     }
 
+    @Override
+    public boolean supportsSearch() {
+        return true;
+    }
+
+    @Override
+    public List<Model> search(String q) throws IOException, InterruptedException {
+        String url = BASE_URI + "?keywords=" + URLEncoder.encode(q, "utf-8");
+        List<Model> result = new ArrayList<>();
+
+        // search online models
+        Request req = new Request.Builder()
+                .url(url)
+                .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
+                .build();
+        try(Response resp = getHttpClient().execute(req)) {
+            if(resp.isSuccessful()) {
+                result.addAll(ChaturbateModelParser.parseModels(this, resp.body().string()));
+            }
+        }
+
+        // since chaturbate does not return offline models, we at least try, if the profile page
+        // exists for the search string
+        url = BASE_URI + '/' + q;
+        req = new Request.Builder()
+                .url(url)
+                .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
+                .build();
+        try(Response resp = getHttpClient().execute(req)) {
+            if(resp.isSuccessful()) {
+                Model model = createModel(q);
+                result.add(model);
+            }
+        }
+
+        return result;
+    }
+
     @Override
     public boolean isSiteForModel(Model m) {
         return m instanceof ChaturbateModel;
diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java
index bb0d4c13..146c834a 100644
--- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java
+++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java
@@ -1,6 +1,7 @@
 package ctbrec.sites.mfc;
 
 import java.io.IOException;
+import java.util.List;
 
 import org.jsoup.select.Elements;
 
@@ -97,6 +98,16 @@ public class MyFreeCams extends AbstractSite {
         return true;
     }
 
+    @Override
+    public boolean supportsSearch() {
+        return true;
+    }
+
+    @Override
+    public List<Model> search(String q) throws IOException, InterruptedException {
+        return client.search(q);
+    }
+
     @Override
     public boolean isSiteForModel(Model m) {
         return m instanceof MyFreeCamsModel;
diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java
index 78b9e864..04c168c4 100644
--- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java
+++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java
@@ -7,7 +7,9 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.ExecutorService;
@@ -15,6 +17,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
@@ -60,6 +63,8 @@ public class MyFreeCamsClient {
     private int sessionId;
     private long heartBeat;
     private volatile boolean connecting = false;
+    private static int messageId = 31415; // starting with 31415 just for fun
+    private Map<Integer, Consumer<Message>> responseHandlers = new HashMap<>();
 
     private EvictingQueue<String> receivedTextHistory = EvictingQueue.create(100);
 
@@ -193,6 +198,7 @@ public class MyFreeCamsClient {
                         case LOGIN:
                             LOG.debug("LOGIN: {}", message);
                             sessionId = message.getReceiver();
+                            LOG.debug("Session ID {}", sessionId);
                             break;
                         case DETAILS:
                         case ROOMHELPER:
@@ -201,7 +207,6 @@ public class MyFreeCamsClient {
                         case CMESG:
                         case PMESG:
                         case TXPROFILE:
-                        case USERNAMELOOKUP:
                         case MYCAMSTATE:
                         case MYWEBCAM:
                         case JOINCHAN:
@@ -217,6 +222,18 @@ public class MyFreeCamsClient {
                                 }
                             }
                             break;
+                        case USERNAMELOOKUP:
+                            //                            LOG.debug("{}", message.getType());
+                            //                            LOG.debug("{}", message.getSender());
+                            //                            LOG.debug("{}", message.getReceiver());
+                            //                            LOG.debug("{}", message.getArg1());
+                            //                            LOG.debug("{}", message.getArg2());
+                            //                            LOG.debug("{}", message.getMessage());
+                            Consumer<Message> responseHandler = responseHandlers.remove(message.getArg1());
+                            if(responseHandler != null) {
+                                responseHandler.accept(message);
+                            }
+                            break;
                         case TAGS:
                             JSONObject json = new JSONObject(message.getMessage());
                             String[] names = JSONObject.getNames(json);
@@ -571,4 +588,34 @@ public class MyFreeCamsClient {
     public ServerConfig getServerConfig() {
         return serverConfig;
     }
+
+    public List<ctbrec.Model> search(String q) throws InterruptedException {
+        LOG.debug("Sending USERNAMELOOKUP for {}", q);
+        int msgId = messageId++;
+        Object monitor = new Object();
+        List<ctbrec.Model> result = new ArrayList<>();
+        responseHandlers.put(msgId, msg -> {
+            LOG.debug("Search result: " + msg);
+            if(StringUtil.isNotBlank(msg.getMessage()) && !Objects.equals(msg.getMessage(), q)) {
+                JSONObject json = new JSONObject(msg.getMessage());
+                String name = json.getString("nm");
+                MyFreeCamsModel model = mfc.createModel(name);
+                model.setUid(json.getInt("uid"));
+                model.setState(State.of(json.getInt("vs")));
+                String uid = Integer.toString(model.getUid());
+                String uidStart = uid.substring(0, 3);
+                String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.90x90.jpg";
+                model.setPreview(previewUrl);
+                result.add(model);
+            }
+            synchronized (monitor) {
+                monitor.notify();
+            }
+        });
+        ws.send("10 " + sessionId + " 0 " + msgId + " 0 " + q + "\n");
+        synchronized (monitor) {
+            monitor.wait();
+        }
+        return result;
+    }
 }