760 lines
29 KiB
Java
760 lines
29 KiB
Java
package ctbrec.ui.tabs;
|
|
|
|
import static ctbrec.Model.State.*;
|
|
import static ctbrec.io.HttpConstants.*;
|
|
import static ctbrec.ui.Icon.*;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Locale;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.function.Consumer;
|
|
import java.util.stream.Collectors;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import com.google.common.cache.CacheBuilder;
|
|
import com.google.common.cache.CacheLoader;
|
|
import com.google.common.cache.LoadingCache;
|
|
|
|
import ctbrec.Config;
|
|
import ctbrec.GlobalThreadPool;
|
|
import ctbrec.Model;
|
|
import ctbrec.io.HttpException;
|
|
import ctbrec.recorder.Recorder;
|
|
import ctbrec.ui.AutosizeAlert;
|
|
import ctbrec.ui.CamrecApplication;
|
|
import ctbrec.ui.Icon;
|
|
import ctbrec.ui.SiteUiFactory;
|
|
import ctbrec.ui.StreamSourceSelectionDialog;
|
|
import ctbrec.ui.action.EditGroupAction;
|
|
import ctbrec.ui.action.PlayAction;
|
|
import ctbrec.ui.controls.Dialogs;
|
|
import ctbrec.ui.controls.RecordingIndicator;
|
|
import ctbrec.ui.controls.StreamPreview;
|
|
import javafx.animation.FadeTransition;
|
|
import javafx.animation.FillTransition;
|
|
import javafx.animation.ParallelTransition;
|
|
import javafx.animation.Transition;
|
|
import javafx.application.Platform;
|
|
import javafx.beans.property.BooleanProperty;
|
|
import javafx.beans.property.SimpleBooleanProperty;
|
|
import javafx.beans.value.ObservableValue;
|
|
import javafx.collections.ObservableList;
|
|
import javafx.geometry.Insets;
|
|
import javafx.geometry.Pos;
|
|
import javafx.scene.Cursor;
|
|
import javafx.scene.Node;
|
|
import javafx.scene.control.Alert;
|
|
import javafx.scene.control.ContextMenu;
|
|
import javafx.scene.control.Label;
|
|
import javafx.scene.control.Tooltip;
|
|
import javafx.scene.image.Image;
|
|
import javafx.scene.image.ImageView;
|
|
import javafx.scene.input.MouseEvent;
|
|
import javafx.scene.layout.StackPane;
|
|
import javafx.scene.paint.Color;
|
|
import javafx.scene.paint.Paint;
|
|
import javafx.scene.shape.Circle;
|
|
import javafx.scene.shape.Polygon;
|
|
import javafx.scene.shape.Rectangle;
|
|
import javafx.scene.shape.Shape;
|
|
import javafx.scene.text.Font;
|
|
import javafx.scene.text.Text;
|
|
import javafx.scene.text.TextAlignment;
|
|
import javafx.util.Duration;
|
|
import okhttp3.Request;
|
|
import okhttp3.Response;
|
|
|
|
public class ThumbCell extends StackPane {
|
|
|
|
private static final String COULDNT_START_STOP_RECORDING = "Couldn't start/stop recording";
|
|
private static final String ERROR = "Error";
|
|
private static final Logger LOG = LoggerFactory.getLogger(ThumbCell.class);
|
|
private static final Duration ANIMATION_DURATION = new Duration(250);
|
|
|
|
private static Image imgRecordIndicator = new Image(MEDIA_RECORD_16.url());
|
|
private static Image imgPauseIndicator = new Image(MEDIA_PLAYBACK_PAUSE_16.url());
|
|
private static Image imgBookmarkIndicator = new Image(BOOKMARK_16.url());
|
|
private static Image imgGroupIndicator = new Image(Icon.GROUP_16.url());
|
|
|
|
private ModelRecordingState modelRecordingState = ModelRecordingState.NOT;
|
|
private Model model;
|
|
private StreamPreview streamPreview;
|
|
private ImageView iv;
|
|
private Rectangle resolutionBackground;
|
|
private final Paint resolutionOnlineColor = new Color(0.22, 0.8, 0.29, 1);
|
|
private final Color resolutionOfflineColor = new Color(0.8, 0.28, 0.28, 1);
|
|
private Rectangle nameBackground;
|
|
private Rectangle topicBackground;
|
|
private Rectangle selectionOverlay;
|
|
private Text name;
|
|
private Text topic;
|
|
private Text resolutionTag;
|
|
private Recorder recorder;
|
|
private RecordingIndicator recordingIndicator;
|
|
private Tooltip recordingIndicatorTooltip;
|
|
private StackPane previewTrigger;
|
|
private Label groupIndicatorTooltipTrigger;
|
|
private ImageView groupIndicator;
|
|
private int index = 0;
|
|
ContextMenu popup;
|
|
private static final Color colorNormal = Color.BLACK;
|
|
private static final Color colorHighlight = Color.WHITE;
|
|
private final Color colorRecording = new Color(0.8, 0.28, 0.28, .8);
|
|
private SimpleBooleanProperty selectionProperty = new SimpleBooleanProperty(false);
|
|
private double imgAspectRatio = 3.0 / 4.0;
|
|
private SimpleBooleanProperty preserveAspectRatio = new SimpleBooleanProperty(true);
|
|
|
|
private ObservableList<Node> thumbCellList;
|
|
private boolean mouseHovering = false;
|
|
private boolean recording = false;
|
|
static LoadingCache<Model, int[]> resolutionCache = CacheBuilder.newBuilder()
|
|
.expireAfterAccess(5, TimeUnit.MINUTES)
|
|
.maximumSize(10000)
|
|
.build(CacheLoader.from(ThumbCell::getStreamResolution));
|
|
private ThumbOverviewTab parent;
|
|
private CompletableFuture<Boolean> startPreview;
|
|
|
|
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder, double aspectRatio) {
|
|
this.parent = parent;
|
|
this.thumbCellList = parent.grid.getChildren();
|
|
this.model = model;
|
|
this.recorder = recorder;
|
|
this.imgAspectRatio = aspectRatio;
|
|
recording = recorder.isTracked(model);
|
|
model.setSuspended(recorder.isSuspended(model));
|
|
this.setStyle("-fx-background-color: -fx-base");
|
|
|
|
streamPreview = new StreamPreview();
|
|
streamPreview.prefWidthProperty().bind(widthProperty());
|
|
streamPreview.prefHeightProperty().bind(heightProperty());
|
|
getChildren().add(streamPreview);
|
|
|
|
iv = new ImageView();
|
|
iv.setSmooth(true);
|
|
iv.setPreserveRatio(true);
|
|
getChildren().add(iv);
|
|
|
|
nameBackground = new Rectangle();
|
|
nameBackground.setFill(recording ? colorRecording : colorNormal);
|
|
nameBackground.setOpacity(.7);
|
|
StackPane.setAlignment(nameBackground, Pos.BOTTOM_CENTER);
|
|
getChildren().add(nameBackground);
|
|
|
|
topicBackground = new Rectangle();
|
|
topicBackground.setFill(Color.BLACK);
|
|
topicBackground.setOpacity(0);
|
|
StackPane.setAlignment(topicBackground, Pos.TOP_LEFT);
|
|
getChildren().add(topicBackground);
|
|
|
|
resolutionBackground = new Rectangle(34, 16);
|
|
resolutionBackground.setFill(resolutionOnlineColor);
|
|
resolutionBackground.setVisible(false);
|
|
resolutionBackground.setArcHeight(5);
|
|
resolutionBackground.setArcWidth(resolutionBackground.getArcHeight());
|
|
StackPane.setAlignment(resolutionBackground, Pos.TOP_RIGHT);
|
|
StackPane.setMargin(resolutionBackground, new Insets(2));
|
|
getChildren().add(resolutionBackground);
|
|
|
|
name = new Text(model.getDisplayName());
|
|
name.setFill(Color.WHITE);
|
|
name.setFont(new Font("Sansserif", 16));
|
|
name.setTextAlignment(TextAlignment.CENTER);
|
|
name.prefHeight(25);
|
|
StackPane.setAlignment(name, Pos.BOTTOM_CENTER);
|
|
getChildren().add(name);
|
|
|
|
topic = new Text();
|
|
String txt = recording ? " " : "";
|
|
txt += model.getDescription();
|
|
topic.setText(txt);
|
|
|
|
topic.setFill(Color.WHITE);
|
|
topic.setFont(new Font("Sansserif", 13));
|
|
topic.setTextAlignment(TextAlignment.LEFT);
|
|
topic.setOpacity(0);
|
|
var margin = 4;
|
|
StackPane.setMargin(topic, new Insets(margin));
|
|
StackPane.setAlignment(topic, Pos.TOP_CENTER);
|
|
getChildren().add(topic);
|
|
|
|
resolutionTag = new Text();
|
|
resolutionTag.setFill(Color.WHITE);
|
|
resolutionTag.setVisible(false);
|
|
StackPane.setAlignment(resolutionTag, Pos.TOP_RIGHT);
|
|
StackPane.setMargin(resolutionTag, new Insets(2, 4, 2, 2));
|
|
getChildren().add(resolutionTag);
|
|
|
|
recordingIndicator = new RecordingIndicator(16);
|
|
recordingIndicator.setCursor(Cursor.HAND);
|
|
recordingIndicator.setOnMouseClicked(this::recordingInidicatorClicked);
|
|
recordingIndicatorTooltip = new Tooltip("Pause Recording");
|
|
Tooltip.install(recordingIndicator, recordingIndicatorTooltip);
|
|
StackPane.setMargin(recordingIndicator, new Insets(3));
|
|
StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT);
|
|
getChildren().add(recordingIndicator);
|
|
|
|
groupIndicator = new ImageView(imgGroupIndicator);
|
|
groupIndicator.setVisible(false);
|
|
groupIndicatorTooltipTrigger = new Label();
|
|
groupIndicatorTooltipTrigger.setPrefSize(16, 16);
|
|
groupIndicatorTooltipTrigger.setMinSize(16, 16);
|
|
groupIndicatorTooltipTrigger.visibleProperty().bind(groupIndicator.visibleProperty());
|
|
groupIndicatorTooltipTrigger.setCursor(Cursor.HAND);
|
|
groupIndicatorTooltipTrigger.setOnMouseClicked(e -> new EditGroupAction(this, recorder, model).execute());
|
|
StackPane.setMargin(groupIndicatorTooltipTrigger, new Insets(0, 3, 23, 0));
|
|
StackPane.setAlignment(groupIndicatorTooltipTrigger, Pos.BOTTOM_RIGHT);
|
|
StackPane.setMargin(groupIndicator, new Insets(0, 3, 23, 0));
|
|
StackPane.setAlignment(groupIndicator, Pos.BOTTOM_RIGHT);
|
|
getChildren().add(groupIndicator);
|
|
getChildren().add(groupIndicatorTooltipTrigger);
|
|
|
|
if (Config.getInstance().getSettings().livePreviews) {
|
|
getChildren().add(createPreviewTrigger());
|
|
}
|
|
|
|
selectionOverlay = new Rectangle();
|
|
selectionOverlay.visibleProperty().bind(selectionProperty);
|
|
selectionOverlay.widthProperty().bind(widthProperty());
|
|
selectionOverlay.heightProperty().bind(heightProperty());
|
|
StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT);
|
|
getChildren().add(selectionOverlay);
|
|
|
|
setOnMouseEntered(e -> {
|
|
mouseHovering = true;
|
|
Color normal = recording ? colorRecording : colorNormal;
|
|
new ParallelTransition(changeColor(nameBackground, normal, colorHighlight), changeColor(name, colorHighlight, normal)).playFromStart();
|
|
new ParallelTransition(changeOpacity(topicBackground, 0.7), changeOpacity(topic, 0.7)).playFromStart();
|
|
if (Config.getInstance().getSettings().determineResolution) {
|
|
resolutionBackground.setVisible(false);
|
|
resolutionTag.setVisible(false);
|
|
}
|
|
});
|
|
setOnMouseExited(e -> {
|
|
mouseHovering = false;
|
|
Color normal = recording ? colorRecording : colorNormal;
|
|
new ParallelTransition(changeColor(nameBackground, colorHighlight, normal), changeColor(name, normal, colorHighlight)).playFromStart();
|
|
new ParallelTransition(changeOpacity(topicBackground, 0), changeOpacity(topic, 0)).playFromStart();
|
|
if (Config.getInstance().getSettings().determineResolution && !resolutionTag.getText().isEmpty()) {
|
|
resolutionBackground.setVisible(true);
|
|
resolutionTag.setVisible(true);
|
|
}
|
|
});
|
|
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
|
|
|
setRecording(recording);
|
|
update();
|
|
}
|
|
|
|
private void recordingInidicatorClicked(MouseEvent evt) {
|
|
switch(modelRecordingState) {
|
|
case RECORDING:
|
|
pauseResumeAction(true);
|
|
break;
|
|
case PAUSED:
|
|
pauseResumeAction(false);
|
|
break;
|
|
case BOOKMARKED:
|
|
recordLater(false);
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
|
|
private Node createPreviewTrigger() {
|
|
var s = 32;
|
|
previewTrigger = new StackPane();
|
|
previewTrigger.setStyle("-fx-background-color: white;");
|
|
previewTrigger.setOpacity(.8);
|
|
previewTrigger.setMaxSize(s, s);
|
|
|
|
var play = new Polygon(16, 8, 26, 15, 16, 22);
|
|
StackPane.setMargin(play, new Insets(0, 0, 0, 3));
|
|
play.setStyle("-fx-background-color: black;");
|
|
previewTrigger.getChildren().add(play);
|
|
|
|
var clip = new Circle(s / 2.0);
|
|
clip.setTranslateX(clip.getRadius());
|
|
clip.setTranslateY(clip.getRadius());
|
|
previewTrigger.setClip(clip);
|
|
StackPane.setAlignment(previewTrigger, Pos.BOTTOM_LEFT);
|
|
StackPane.setMargin(previewTrigger, new Insets(0, 0, 24, 4));
|
|
previewTrigger.setOnMouseEntered(evt -> startPreview());
|
|
previewTrigger.setOnMouseExited(evt -> stopPreview());
|
|
return previewTrigger;
|
|
}
|
|
|
|
private void stopPreview() {
|
|
if (startPreview != null) {
|
|
startPreview.cancel(true);
|
|
}
|
|
setPreviewVisible(previewTrigger, false);
|
|
}
|
|
|
|
private void startPreview() {
|
|
previewTrigger.setCursor(Cursor.HAND);
|
|
startPreview = CompletableFuture.supplyAsync(() -> {
|
|
try {
|
|
Thread.sleep(500);
|
|
return true;
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
return false;
|
|
}
|
|
}, GlobalThreadPool.get()).whenComplete((result, exception) -> {
|
|
startPreview = null;
|
|
if (result.booleanValue()) {
|
|
setPreviewVisible(previewTrigger, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void setPreviewVisible(Node previewTrigger, boolean visible) {
|
|
parent.suspendUpdates(visible);
|
|
iv.setVisible(!visible);
|
|
topic.setVisible(!visible);
|
|
topicBackground.setVisible(!visible);
|
|
name.setVisible(!visible);
|
|
nameBackground.setVisible(!visible);
|
|
streamPreview.setVisible(visible);
|
|
if (visible) {
|
|
streamPreview.startStream(model);
|
|
} else {
|
|
streamPreview.stop();
|
|
}
|
|
recordingIndicator.setVisible(!visible);
|
|
if (!visible) {
|
|
updateRecordingIndicator();
|
|
}
|
|
previewTrigger.setCursor(visible ? Cursor.HAND : Cursor.DEFAULT);
|
|
}
|
|
|
|
public void setSelected(boolean selected) {
|
|
selectionProperty.set(selected);
|
|
selectionOverlay.setOpacity(selected ? .75 : 0);
|
|
if (selected) {
|
|
selectionOverlay.getStyleClass().add("selection-background");
|
|
} else {
|
|
selectionOverlay.getStyleClass().remove("selection-background");
|
|
}
|
|
}
|
|
|
|
public boolean isSelected() {
|
|
return selectionProperty.get();
|
|
}
|
|
|
|
public ObservableValue<Boolean> selectionProperty() {
|
|
return selectionProperty;
|
|
}
|
|
|
|
private void updateResolutionTag() {
|
|
ThumbOverviewTab.threadPool.submit(() -> {
|
|
int[] resolution;
|
|
String tagText;
|
|
Paint resolutionBackgroundColor;
|
|
try {
|
|
resolution = resolutionCache.get(model);
|
|
resolutionBackgroundColor = resolutionOnlineColor;
|
|
final int w = resolution[1];
|
|
tagText = w != Integer.MAX_VALUE ? Integer.toString(w) : "HD";
|
|
if (w == 0) {
|
|
var state = model.getOnlineState(false);
|
|
tagText = state.name();
|
|
if (model.isOnline() && state == ONLINE) {
|
|
resolutionCache.invalidate(model);
|
|
} else {
|
|
resolutionBackgroundColor = resolutionOfflineColor;
|
|
if (state == ONLINE) {
|
|
// state can't be ONLINE while the model is offline
|
|
tagText = OFFLINE.name();
|
|
}
|
|
}
|
|
} else {
|
|
var state = model.getOnlineState(true);
|
|
if (state != ONLINE) {
|
|
tagText = state.name();
|
|
resolutionBackgroundColor = resolutionOfflineColor;
|
|
}
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
tagText = "error";
|
|
resolutionBackgroundColor = resolutionOfflineColor;
|
|
} catch (ExecutionException | IOException e) {
|
|
tagText = "error";
|
|
resolutionBackgroundColor = resolutionOfflineColor;
|
|
}
|
|
|
|
final String text = tagText;
|
|
final Paint c = resolutionBackgroundColor;
|
|
Platform.runLater(() -> {
|
|
String oldText = resolutionTag.getText();
|
|
resolutionTag.setText(text);
|
|
if (!mouseHovering) {
|
|
resolutionTag.setVisible(true);
|
|
resolutionBackground.setVisible(true);
|
|
}
|
|
resolutionBackground.setWidth(resolutionTag.getBoundsInLocal().getWidth() + 4);
|
|
resolutionBackground.setFill(c);
|
|
if (!Objects.equals(oldText, text)) {
|
|
parent.filter();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private void setImage(String url) {
|
|
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
|
boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails;
|
|
if (updateThumbs || iv.getImage() == null) {
|
|
GlobalThreadPool.submit(createThumbDownload(url));
|
|
}
|
|
}
|
|
}
|
|
|
|
private Runnable createThumbDownload(String url) {
|
|
return () -> {
|
|
Request req = new Request.Builder()
|
|
.url(url)
|
|
.header(ACCEPT, "*/*")
|
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
|
.header(REFERER, getModel().getSite().getBaseUrl())
|
|
.build();
|
|
try (Response resp = CamrecApplication.httpClient.execute(req)) {
|
|
if (resp.isSuccessful()) {
|
|
double width = 480;
|
|
double height = width * imgAspectRatio;
|
|
var img = new Image(resp.body().byteStream(), width, height, preserveAspectRatio.get(), true);
|
|
if (img.progressProperty().get() == 1.0) {
|
|
Platform.runLater(() -> {
|
|
iv.setImage(img);
|
|
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
|
});
|
|
} else {
|
|
img.progressProperty().addListener((observable, oldValue, newValue) -> {
|
|
if (newValue.doubleValue() == 1.0) {
|
|
iv.setImage(img);
|
|
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
throw new HttpException(resp.code(), resp.message());
|
|
}
|
|
} catch (IOException e) {
|
|
LOG.warn("Error loading thumbnail: {}", e.getLocalizedMessage());
|
|
}
|
|
};
|
|
}
|
|
|
|
Image getImage() {
|
|
return iv.getImage();
|
|
}
|
|
|
|
private Transition changeColor(Shape shape, Color from, Color to) {
|
|
var transition = new FillTransition(ANIMATION_DURATION, from, to);
|
|
transition.setShape(shape);
|
|
return transition;
|
|
}
|
|
|
|
private Transition changeOpacity(Shape shape, double opacity) {
|
|
var transition = new FadeTransition(ANIMATION_DURATION, shape);
|
|
transition.setFromValue(shape.getOpacity());
|
|
transition.setToValue(opacity);
|
|
return transition;
|
|
}
|
|
|
|
void startPlayer() {
|
|
new PlayAction(this, model).execute();
|
|
}
|
|
|
|
private void setRecording(boolean recording) {
|
|
this.recording = recording;
|
|
Color c;
|
|
if (recording) {
|
|
c = mouseHovering ? colorHighlight : colorRecording;
|
|
} else {
|
|
c = mouseHovering ? colorHighlight : colorNormal;
|
|
}
|
|
nameBackground.setFill(c);
|
|
updateRecordingIndicator();
|
|
}
|
|
|
|
private void updateRecordingIndicator() {
|
|
if (recording) {
|
|
recordingIndicator.setVisible(true);
|
|
if (model.isSuspended()) {
|
|
modelRecordingState = ModelRecordingState.PAUSED;
|
|
recordingIndicator.setImage(imgPauseIndicator);
|
|
recordingIndicatorTooltip.setText("Resume Recording");
|
|
} else {
|
|
modelRecordingState = ModelRecordingState.RECORDING;
|
|
recordingIndicator.setImage(imgRecordIndicator);
|
|
recordingIndicatorTooltip.setText("Pause Recording");
|
|
}
|
|
} else {
|
|
if (model.isMarkedForLaterRecording()) {
|
|
recordingIndicator.setVisible(true);
|
|
modelRecordingState = ModelRecordingState.BOOKMARKED;
|
|
recordingIndicator.setImage(imgBookmarkIndicator);
|
|
recordingIndicatorTooltip.setText("Forget Model");
|
|
} else {
|
|
recordingIndicator.setVisible(false);
|
|
modelRecordingState = ModelRecordingState.NOT;
|
|
recordingIndicator.setImage(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
void startStopAction(boolean start) {
|
|
setCursor(Cursor.WAIT);
|
|
|
|
boolean selectSource = Config.getInstance().getSettings().chooseStreamQuality;
|
|
if (selectSource && start) {
|
|
Consumer<Model> onSuccess = modl -> startStopActionAsync(modl, true);
|
|
Consumer<Throwable> onFail = throwable -> {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getScene());
|
|
alert.setTitle(ERROR);
|
|
alert.setHeaderText(COULDNT_START_STOP_RECORDING);
|
|
alert.setContentText("I/O error while starting/stopping the recording: " + throwable.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
};
|
|
StreamSourceSelectionDialog.show(getScene(), model, onSuccess, onFail);
|
|
} else {
|
|
startStopActionAsync(model, start);
|
|
}
|
|
}
|
|
|
|
void pauseResumeAction(boolean pause) {
|
|
setCursor(Cursor.WAIT);
|
|
GlobalThreadPool.submit(() -> {
|
|
try {
|
|
if (pause) {
|
|
recorder.suspendRecording(model);
|
|
} else {
|
|
recorder.resumeRecording(model);
|
|
}
|
|
setRecording(recording);
|
|
} catch (Exception e1) {
|
|
LOG.error("Couldn't pause/resume recording", e1);
|
|
Platform.runLater(() -> {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getScene());
|
|
alert.setTitle(ERROR);
|
|
alert.setHeaderText("Couldn't pause/resume recording");
|
|
alert.setContentText("I/O error while pausing/resuming the recording: " + e1.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
});
|
|
} finally {
|
|
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
|
|
}
|
|
});
|
|
}
|
|
|
|
private void startStopActionAsync(Model model, boolean start) {
|
|
GlobalThreadPool.submit(() -> {
|
|
try {
|
|
if (start) {
|
|
recorder.addModel(model);
|
|
setRecording(!model.isMarkedForLaterRecording());
|
|
} else {
|
|
recorder.stopRecording(model);
|
|
setRecording(false);
|
|
}
|
|
update();
|
|
} catch (Exception e1) {
|
|
LOG.error(COULDNT_START_STOP_RECORDING, e1);
|
|
Dialogs.showError(getScene(), COULDNT_START_STOP_RECORDING, "I/O error while starting/stopping the recording: ", e1);
|
|
} finally {
|
|
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
|
|
}
|
|
});
|
|
}
|
|
|
|
CompletableFuture<Boolean> follow(boolean follow) {
|
|
setCursor(Cursor.WAIT);
|
|
return CompletableFuture.supplyAsync(() -> {
|
|
try {
|
|
if (follow) {
|
|
SiteUiFactory.getUi(model.getSite()).login();
|
|
boolean followed = model.follow();
|
|
if (followed) {
|
|
return true;
|
|
} else {
|
|
Dialogs.showError(getScene(), "Couldn't follow model", "", null);
|
|
return false;
|
|
}
|
|
} else {
|
|
SiteUiFactory.getUi(model.getSite()).login();
|
|
boolean unfollowed = model.unfollow();
|
|
if (unfollowed) {
|
|
Platform.runLater(() -> thumbCellList.remove(ThumbCell.this));
|
|
return true;
|
|
} else {
|
|
Dialogs.showError(getScene(), "Couldn't unfollow model", "", null);
|
|
return false;
|
|
}
|
|
}
|
|
} catch (Exception e1) {
|
|
LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1);
|
|
String msg = "I/O error while following/unfollowing model " + model.getName() + ": ";
|
|
Dialogs.showError(getScene(), "Couldn't follow/unfollow model", msg, e1);
|
|
return false;
|
|
} finally {
|
|
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
|
|
}
|
|
}, GlobalThreadPool.get());
|
|
}
|
|
|
|
void recordLater(boolean recordLater) {
|
|
model.setMarkedForLaterRecording(recordLater);
|
|
startStopAction(recordLater);
|
|
}
|
|
|
|
public Model getModel() {
|
|
return model;
|
|
}
|
|
|
|
public void setModel(Model model) {
|
|
this.model.setName(model.getName());
|
|
this.model.setDescription(model.getDescription());
|
|
this.model.setPreview(model.getPreview());
|
|
this.model.setTags(model.getTags());
|
|
this.model.setUrl(model.getUrl());
|
|
update();
|
|
}
|
|
|
|
public int getIndex() {
|
|
return index;
|
|
}
|
|
|
|
public void setIndex(int index) {
|
|
this.index = index;
|
|
}
|
|
|
|
protected void update() {
|
|
model.setSuspended(recorder.isSuspended(model));
|
|
model.setMarkedForLaterRecording(recorder.isMarkedForLaterRecording(model));
|
|
setRecording(recorder.isTracked(model));
|
|
updateRecordingIndicator();
|
|
setImage(model.getPreview());
|
|
String txt = (modelRecordingState != ModelRecordingState.NOT) ? " " : "";
|
|
txt += model.getDescription() != null ? model.getDescription() : "";
|
|
topic.setText(txt);
|
|
recorder.getModelGroup(model).ifPresentOrElse(group -> {
|
|
var tooltip = group.getName() + ": " + group.getModelUrls().size() + " models:\n";
|
|
tooltip += group.getModelUrls().stream().collect(Collectors.joining("\n"));
|
|
groupIndicatorTooltipTrigger.setTooltip(new Tooltip(tooltip));
|
|
groupIndicator.setVisible(true);
|
|
}, () -> groupIndicator.setVisible(false));
|
|
|
|
if (Config.getInstance().getSettings().determineResolution) {
|
|
updateResolutionTag();
|
|
} else {
|
|
resolutionBackground.setVisible(false);
|
|
resolutionTag.setVisible(false);
|
|
}
|
|
|
|
requestLayout();
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
final var prime = 31;
|
|
var result = 1;
|
|
result = prime * result + ((model == null) ? 0 : model.hashCode());
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (this == obj)
|
|
return true;
|
|
if (obj == null)
|
|
return false;
|
|
if (getClass() != obj.getClass())
|
|
return false;
|
|
ThumbCell other = (ThumbCell) obj;
|
|
if (model == null) {
|
|
if (other.model != null)
|
|
return false;
|
|
} else if (!model.equals(other.model))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
public void setThumbWidth(int width) {
|
|
int height = (int) (width * imgAspectRatio);
|
|
setSize(width, height);
|
|
iv.prefHeight(width);
|
|
iv.prefWidth(height);
|
|
}
|
|
|
|
private void setSize(int w, int h) {
|
|
if (iv.getImage() != null) {
|
|
double aspectRatio = iv.getImage().getWidth() / iv.getImage().getHeight();
|
|
if (aspectRatio > 1) {
|
|
iv.setFitWidth(w);
|
|
} else {
|
|
iv.setFitHeight(h);
|
|
}
|
|
}
|
|
setMinSize(w, h);
|
|
setPrefSize(w, h);
|
|
nameBackground.setWidth(w);
|
|
nameBackground.setHeight(20);
|
|
topicBackground.setWidth(w);
|
|
topicBackground.setHeight(h - nameBackground.getHeight());
|
|
topic.prefHeight(getHeight() - 25);
|
|
topic.maxHeight(getHeight() - 25);
|
|
var margin = 4;
|
|
topic.maxWidth(w - margin * 2.0);
|
|
topic.setWrappingWidth(w - margin * 2.0);
|
|
|
|
streamPreview.resizeTo(w, h);
|
|
|
|
var clip = new Rectangle(w, h);
|
|
clip.setArcWidth(10);
|
|
clip.arcHeightProperty().bind(clip.arcWidthProperty());
|
|
this.setClip(clip);
|
|
}
|
|
|
|
private static int[] getStreamResolution(Model model) {
|
|
try {
|
|
return model.getStreamResolution(false);
|
|
} catch (ExecutionException e) {
|
|
LOG.trace("Error loading stream resolution for model {}: {}", model, e.getLocalizedMessage());
|
|
return new int[2];
|
|
}
|
|
}
|
|
|
|
public void releaseResources() {
|
|
iv.setImage(null);
|
|
}
|
|
|
|
public void setImageAspectRatio(double imageAspectRatio) {
|
|
this.imgAspectRatio = imageAspectRatio;
|
|
}
|
|
|
|
public BooleanProperty preserveAspectRatioProperty() {
|
|
return preserveAspectRatio;
|
|
}
|
|
|
|
void addInPausedState() {
|
|
model.setSuspended(true);
|
|
model.setMarkedForLaterRecording(false);
|
|
startStopAction(true);
|
|
}
|
|
|
|
private enum ModelRecordingState {
|
|
RECORDING,
|
|
PAUSED,
|
|
BOOKMARKED,
|
|
NOT
|
|
}
|
|
}
|