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:
parent
2202dc969f
commit
b9f24a209e
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ctbrec.ui.autofilltextbox;
|
||||
package ctbrec.ui.controls;
|
||||
|
||||
|
||||
import javafx.collections.ObservableList;
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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... >" 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package ctbrec.ui.controls;
|
||||
|
||||
public class SearchPopover extends Popover {
|
||||
|
||||
|
||||
public SearchPopover() {
|
||||
getStyleClass().add("right-tooth");
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue