Implement search feature

If a site supports searching, add a search field on the right side next
to the filter input field. This search uses the sites search function
to look for models and returns a list of matches in a popup window
This commit is contained in:
0xboobface 2018-11-23 20:27:49 +01:00
parent 2202dc969f
commit b9f24a209e
25 changed files with 1545 additions and 23 deletions

View File

@ -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());

View File

@ -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;

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -1,4 +1,4 @@
package ctbrec.ui.autofilltextbox;
package ctbrec.ui.controls;
import javafx.collections.ObservableList;

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
package ctbrec.ui.controls;
public class SearchPopover extends Popover {
public SearchPopover() {
getStyleClass().add("right-tooth");
}
}

View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}