ctbrec-5.3.2-experimental/src/main/java/ctbrec/ui/ThumbOverviewTab.java

395 lines
14 KiB
Java

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