Merge branch 'dev' into fc2

# Conflicts:
#	common/src/main/java/ctbrec/io/HttpClient.java
This commit is contained in:
0xboobface 2018-12-17 13:05:11 +01:00
commit a0a083aaf6
158 changed files with 9533 additions and 1761 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Desktop (please complete the following information):**
- OS: [e.g. Windows, Mac, Linux]
- Ctbrec Version [e.g. 1.12.1 JRE]
- Standalone or Client / Server mode
**Log**
If there are any errors in the ctbrec.log, please add them here. You can find the ctbrec.log next to the ctbrec.exe.

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

7
.github/ISSUE_TEMPLATE/other.md vendored Normal file
View File

@ -0,0 +1,7 @@
---
name: Other
about: Anything else
---

View File

@ -1,3 +1,78 @@
1.16.0
========================
* Thumbnails can show a live preview. Can be switched off in the settings.
* Added Streamate (metcams, xhamstercams, pornhublive)
* Maximum resolution can be an arbitrary value now
* Added setting for minimal recording length. Recordings, which are shorter
than this value, get deleted automatically.
* Double-click in Recording tab starts the player
* Fix: BongaCams friends tab not working
* Fix: In some cases MFC models got confused
1.15.0
========================
* Fix: BongaCams overview didn't work anymore
* Fix: CamSoda overview didn't work anymore
* Fix: Multi selection of thumbnails didn't work when a tab was opened the
first time
* Fix: Cam4 online detection was to restrictive
* Added tabular view for MFC, which shows all online models
1.14.0
========================
* Added setting for MFC to ignore the upscaled (960p) stream
* Added event system. You can define to show a notification, play a sound or
execute a program, when the state of a model or recording changes
* Added "follow" menu entry on the Recording tab
* Fix: Recordings change from suspended to recording by their own when a
thumbnail tab is opened and the model is showing
* Fix: Linux scripts don't work on systems where bash isn't the default shell
* Improved loading and display of resolution tags. They are not re-loaded
everytime you switch between tabs
1.13.0
========================
* Added possibility to open small live previews of online models
in the Recording tab
* Added setting to toggle "Player Starting" message
* Added possibility to add models by their URL
* Added pause / resume all buttons
* Setting to define the base URL for MFC and CTB
* The paused checkbox are now clickable
* Implemented multi-selection for Recording and Recordings tab
* Fix: Don't throw exceptions for unknown attributes in PlaylistParser
* Fix: Don't do space check, if minimum is set to 0
* Fix: Player not starting when path contains spaces
1.12.1
========================
* Fixed downloads in client / server mode
1.12.0
========================
* Added threshold setting to keep free space on the recording device.
This is useful, if you don't want to use up all of your storage.
The free space is also shown on the recordings tab
* Tweaked the download internals a lot. Downloads should not hang
in RECORDING state without actually recording. Downloads should
be more robust in general.
* Fixed and improved split recordings
* Improved detection of online state for Cam4 models
* Accelerated the initial loading of the "Recording" tab for many
Chaturbate models
* Recordings tab now shows smaller size units (Bytes, KiB, MiB, GiB)
1.11.0
========================
* Added model search function
* Added color settings to change the appearance of the application
* Added setting for the online check interval
* Added setting to define the tab the application opens on start
* Double-click starts playback of recordings
* Refresh of thumbnails can be disabled
* Changed settings are saved immediately (including changes of the
list of recorded models)
1.10.0
========================
* Fix: HMAC authentication didn't work for playing and downloading of a

2
client/.gitignore vendored
View File

@ -2,7 +2,7 @@
/target/
*~
*.bak
/ctbrec.log
/*.log
/ctbrec-tunnel.sh
/jre/
/server-local.sh

View File

@ -8,7 +8,7 @@
<parent>
<groupId>ctbrec</groupId>
<artifactId>master</artifactId>
<version>1.10.0</version>
<version>1.15.0</version>
<relativePath>../master</relativePath>
</parent>
@ -118,10 +118,11 @@
<addDependencies>false</addDependencies>
<preCp>anything</preCp>
</classPath>
<downloadUrl>https://jdk.java.net/</downloadUrl>
<jre>
<path>jre</path>
<bundledJre64Bit>true</bundledJre64Bit>
<minVersion>1.8.0</minVersion>
<minVersion>10</minVersion>
<maxHeapSize>512</maxHeapSize>
</jre>
<versionInfo>

View File

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
pushd $(dirname $0)
JAVA=./jre/bin/java

View File

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
pushd $(dirname $0)
JAVA=java

View File

@ -1,6 +1,9 @@
package ctbrec.ui;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -9,21 +12,24 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import ctbrec.Config;
import ctbrec.StringUtil;
import ctbrec.Version;
import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.io.HttpClient;
import ctbrec.recorder.LocalRecorder;
import ctbrec.recorder.OnlineMonitor;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder;
import ctbrec.sites.Site;
@ -33,6 +39,8 @@ import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.settings.SettingsTab;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.application.Platform;
@ -43,6 +51,7 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import okhttp3.Request;
import okhttp3.Response;
@ -53,12 +62,13 @@ public class CamrecApplication extends Application {
private Config config;
private Recorder recorder;
private OnlineMonitor onlineMonitor;
static HostServices hostServices;
private SettingsTab settingsTab;
private TabPane rootPane = new TabPane();
static EventBus bus;
private List<Site> sites = new ArrayList<>();
public static HttpClient httpClient;
public static String title;
@Override
public void start(Stage primaryStage) throws Exception {
@ -69,11 +79,14 @@ public class CamrecApplication extends Application {
sites.add(new Chaturbate());
sites.add(new Fc2Live());
sites.add(new MyFreeCams());
sites.add(new Streamate());
loadConfig();
registerAlertSystem();
createHttpClient();
bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
hostServices = getHostServices();
createRecorder();
onlineMonitor = new OnlineMonitor(recorder);
onlineMonitor.start();
for (Site site : sites) {
if(site.isEnabled()) {
try {
@ -96,7 +109,8 @@ public class CamrecApplication extends Application {
private void createGui(Stage primaryStage) throws IOException {
LOG.debug("Creating GUI");
primaryStage.setTitle("CTB Recorder " + getVersion());
CamrecApplication.title = "CTB Recorder " + getVersion();
primaryStage.setTitle(title);
InputStream icon = getClass().getResourceAsStream("/icon.png");
primaryStage.getIcons().add(new Image(icon));
int windowWidth = Config.getInstance().getSettings().windowWidth;
@ -112,19 +126,26 @@ public class CamrecApplication extends Application {
rootPane.getTabs().add(siteTab);
}
}
try {
((SiteTab)rootPane.getTabs().get(0)).selected();
} catch(ClassCastException | IndexOutOfBoundsException e) {}
RecordedModelsTab modelsTab = new RecordedModelsTab("Recording", recorder, sites);
rootPane.getTabs().add(modelsTab);
RecordingsTab recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
rootPane.getTabs().add(recordingsTab);
settingsTab = new SettingsTab(sites);
settingsTab = new SettingsTab(sites, recorder);
rootPane.getTabs().add(settingsTab);
rootPane.getTabs().add(new DonateTabFx());
switchToStartTab();
writeColorSchemeStyleSheet(primaryStage);
Color base = Color.web(Config.getInstance().getSettings().colorBase);
if(!base.equals(Color.WHITE)) {
loadStyleSheet(primaryStage, "color.css");
}
loadStyleSheet(primaryStage, "style.css");
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/ColorSettingsPane.css");
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());
@ -146,7 +167,10 @@ public class CamrecApplication extends Application {
new Thread() {
@Override
public void run() {
modelsTab.saveState();
recordingsTab.saveState();
settingsTab.saveConfig();
onlineMonitor.shutdown();
recorder.shutdown();
for (Site site : sites) {
if(site.isEnabled()) {
@ -156,9 +180,13 @@ public class CamrecApplication extends Application {
try {
Config.getInstance().save();
LOG.info("Shutdown complete. Goodbye!");
Platform.runLater(() -> {
primaryStage.close();
shutdownInfo.close();
Platform.exit();
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
System.exit(0);
});
} catch (IOException e1) {
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
@ -186,6 +214,64 @@ public class CamrecApplication extends Application {
});
}
private void registerAlertSystem() {
new Thread(() -> {
try {
// don't register before 1 minute has passed, because directly after
// the start of ctbrec, an event for every online model would be fired,
// which is annoying as f
Thread.sleep(TimeUnit.MINUTES.toMillis(1));
for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) {
EventHandler handler = new EventHandler(config);
EventBusHolder.register(handler);
LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName());
}
LOG.debug("Alert System registered");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
private void writeColorSchemeStyleSheet(Stage primaryStage) {
File colorCss = new File(Config.getInstance().getConfigDir(), "color.css");
try(FileOutputStream fos = new FileOutputStream(colorCss)) {
String content = ".root {\n" +
" -fx-base: "+Config.getInstance().getSettings().colorBase+";\n" +
" -fx-accent: "+Config.getInstance().getSettings().colorAccent+";\n" +
" -fx-default-button: -fx-accent;\n" +
" -fx-focus-color: -fx-accent;\n" +
" -fx-control-inner-background-alt: derive(-fx-base, 95%);\n" +
"}";
fos.write(content.getBytes("utf-8"));
} catch(Exception e) {
LOG.error("Couldn't write stylesheet for user defined color theme");
}
}
private void loadStyleSheet(Stage primaryStage, String filename) {
File css = new File(Config.getInstance().getConfigDir(), filename);
if(css.exists() && css.isFile()) {
primaryStage.getScene().getStylesheets().add(css.toURI().toString());
}
}
private void switchToStartTab() {
String startTab = Config.getInstance().getSettings().startTab;
if(StringUtil.isNotBlank(startTab)) {
for (Tab tab : rootPane.getTabs()) {
if(Objects.equals(startTab, tab.getText())) {
rootPane.getSelectionModel().select(tab);
break;
}
}
}
if(rootPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) {
((TabSelectionListener)rootPane.getSelectionModel().getSelectedItem()).selected();
}
}
private void createRecorder() {
if (config.getSettings().localRecording) {
recorder = new LocalRecorder(config);
@ -201,9 +287,8 @@ public class CamrecApplication extends Application {
LOG.error("Couldn't load settings", e);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't load settings.");
alert.setContentText("Couldn't load settings. Falling back to defaults. A backup of your settings has been created.");
alert.showAndWait();
System.exit(1);
}
config = Config.getInstance();
}

View File

@ -9,13 +9,9 @@ import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
@ -26,7 +22,6 @@ public class DonateTabFx extends Tab {
setText("Donate");
BorderPane container = new BorderPane();
container.setPadding(new Insets(10));
container.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(0))));
setContent(container);
VBox headerVbox = new VBox(10);
@ -53,21 +48,21 @@ public class DonateTabFx extends Tab {
tokenDesc.setTextAlignment(TextAlignment.CENTER);
tokenBox.getChildren().addAll(tokenImage, tokenButton, tokenDesc);
ImageView coffeeImage = new ImageView(getClass().getResource("/html/buymeacoffee-fancy.png").toString());
ImageView coffeeImage = new ImageView(getClass().getResource("/buymeacoffee-round.png").toString());
Button coffeeButton = new Button("Buy me a coffee");
coffeeButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.buymeacoffee.com/0xboobface"); });
VBox buyCoffeeBox = new VBox(5);
buyCoffeeBox.setAlignment(Pos.TOP_CENTER);
buyCoffeeBox.getChildren().addAll(coffeeImage, coffeeButton);
ImageView paypalImage = new ImageView(getClass().getResource("/html/pp196.png").toString());
ImageView paypalImage = new ImageView(getClass().getResource("/paypal-round.png").toString());
Button paypalButton = new Button("PayPal");
paypalButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.paypal.me/0xb00bface"); });
VBox paypalBox = new VBox(5);
paypalBox.setAlignment(Pos.TOP_CENTER);
paypalBox.getChildren().addAll(paypalImage, paypalButton);
ImageView patreonImage = new ImageView(getClass().getResource("/html/patreon-logo.png").toString());
ImageView patreonImage = new ImageView(getClass().getResource("/patreon-round.png").toString());
Button patreonButton = new Button("Become a Patron");
patreonButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.patreon.com/0xb00bface"); });
VBox patreonBox = new VBox(5);

View File

@ -110,7 +110,7 @@ public class JavaFxModel implements Model {
}
@Override
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
return delegate.getOnlineState(failFast);
}
@ -197,4 +197,19 @@ public class JavaFxModel implements Model {
delegate.setSuspended(suspended);
pausedProperty.set(suspended);
}
@Override
public String getDisplayName() {
return delegate.getDisplayName();
}
@Override
public void setDisplayName(String name) {
delegate.setDisplayName(name);
}
@Override
public int compareTo(Model o) {
return delegate.compareTo(o);
}
}

View File

@ -1,10 +1,11 @@
package ctbrec.ui;
import java.text.DecimalFormat;
import java.time.Instant;
import ctbrec.Config;
import ctbrec.Recording;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@ -12,9 +13,10 @@ public class JavaFxRecording extends Recording {
private transient StringProperty statusProperty = new SimpleStringProperty();
private transient StringProperty progressProperty = new SimpleStringProperty();
private transient StringProperty sizeProperty = new SimpleStringProperty();
private transient LongProperty sizeProperty = new SimpleLongProperty();
private Recording delegate;
private long lastValue = 0;
public JavaFxRecording(Recording recording) {
this.delegate = recording;
@ -41,7 +43,7 @@ public class JavaFxRecording extends Recording {
}
@Override
public STATUS getStatus() {
public State getStatus() {
return delegate.getStatus();
}
@ -50,7 +52,7 @@ public class JavaFxRecording extends Recording {
}
@Override
public void setStatus(STATUS status) {
public void setStatus(State status) {
delegate.setStatus(status);
switch(status) {
case RECORDING:
@ -65,8 +67,14 @@ public class JavaFxRecording extends Recording {
case DOWNLOADING:
statusProperty.set("downloading");
break;
case MERGING:
statusProperty.set("merging");
case POST_PROCESSING:
statusProperty.set("post-processing");
break;
case STOPPED:
statusProperty.set("stopped");
break;
case UNKNOWN:
statusProperty.set("unknown");
break;
}
}
@ -89,9 +97,7 @@ public class JavaFxRecording extends Recording {
@Override
public void setSizeInByte(long sizeInByte) {
delegate.setSizeInByte(sizeInByte);
double sizeInGiB = sizeInByte / 1024.0 / 1024 / 1024;
DecimalFormat df = new DecimalFormat("0.00");
sizeProperty.setValue(df.format(sizeInGiB) + " GiB");
sizeProperty.set(sizeInByte);
}
public StringProperty getProgressProperty() {
@ -115,7 +121,7 @@ public class JavaFxRecording extends Recording {
public void update(Recording updated) {
if(!Config.getInstance().getSettings().localRecording) {
if(getStatus() == STATUS.DOWNLOADING && updated.getStatus() != STATUS.DOWNLOADING) {
if(getStatus() == State.DOWNLOADING && updated.getStatus() != State.DOWNLOADING) {
// ignore, because the the status coming from the server is FINISHED and we are
// overriding it with DOWNLOADING
return;
@ -151,8 +157,13 @@ public class JavaFxRecording extends Recording {
return delegate.getSizeInByte();
}
public StringProperty getSizeProperty() {
public LongProperty getSizeProperty() {
return sizeProperty;
}
public boolean valueChanged() {
boolean changed = getSizeInByte() != lastValue;
lastValue = getSizeInByte();
return changed;
}
}

View File

@ -23,7 +23,7 @@ public class Player {
private static final transient Logger LOG = LoggerFactory.getLogger(Player.class);
private static PlayerThread playerThread;
public static void play(String url) {
public static boolean play(String url) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
try {
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
@ -31,12 +31,14 @@ public class Player {
}
playerThread = new PlayerThread(url);
return true;
} catch (Exception e1) {
LOG.error("Couldn't start player", e1);
return false;
}
}
public static void play(Recording rec) {
public static boolean play(Recording rec) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
try {
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
@ -44,12 +46,14 @@ public class Player {
}
playerThread = new PlayerThread(rec);
return true;
} catch (Exception e1) {
LOG.error("Couldn't start player", e1);
return false;
}
}
public static void play(Model model) {
public static boolean play(Model model) {
try {
if(model.isOnline(true)) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
@ -60,7 +64,7 @@ public class Player {
Collections.sort(sources);
StreamSource best = sources.get(sources.size()-1);
LOG.debug("Playing {}", best.getMediaPlaylistUrl());
Player.play(best.getMediaPlaylistUrl());
return Player.play(best.getMediaPlaylistUrl());
} else {
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
@ -68,6 +72,7 @@ public class Player {
alert.setHeaderText("Room is currently not public");
alert.showAndWait();
});
return false;
}
} catch (Exception e1) {
LOG.error("Couldn't get stream information for model {}", model, e1);
@ -78,6 +83,7 @@ public class Player {
alert.setContentText(e1.getLocalizedMessage());
alert.showAndWait();
});
return false;
}
}
@ -114,7 +120,11 @@ public class Player {
try {
if (Config.getInstance().getSettings().localRecording && rec != null) {
File file = new File(Config.getInstance().getSettings().recordingsDir, rec.getPath());
playerProcess = rt.exec(Config.getInstance().getSettings().mediaPlayer + " " + file, OS.getEnvironment(), file.getParentFile());
String[] args = new String[] {
Config.getInstance().getSettings().mediaPlayer,
file.getName()
};
playerProcess = rt.exec(args, OS.getEnvironment(), file.getParentFile());
} else {
if(Config.getInstance().getSettings().requireAuthentication) {
URL u = new URL(url);
@ -130,10 +140,12 @@ public class Player {
// create threads, which read stdout and stderr of the player process. these are needed,
// because otherwise the internal buffer for these streams fill up and block the process
Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), new DevNull()));
//Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out));
std.setName("Player stdout pipe");
std.setDaemon(true);
std.start();
Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), new DevNull()));
//Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), System.err));
err.setName("Player stderr pipe");
err.setDaemon(true);
err.start();

View File

@ -0,0 +1,188 @@
package ctbrec.ui;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.ui.controls.StreamPreview;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
public class PreviewPopupHandler implements EventHandler<MouseEvent> {
private static final transient Logger LOG = LoggerFactory.getLogger(PreviewPopupHandler.class);
private static final int offset = 10;
private long timeForPopupOpen = TimeUnit.SECONDS.toMillis(1);
private long timeForPopupClose = 400;
private Popup popup = new Popup();
private Node parent;
private StreamPreview streamPreview;
private JavaFxModel model;
private volatile long openCountdown = -1;
private volatile long closeCountdown = -1;
private volatile long lastModelChange = -1;
private volatile boolean changeModel = false;
public PreviewPopupHandler(Node parent) {
this.parent = parent;
streamPreview = new StreamPreview();
streamPreview.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+
"-fx-background-insets: 0 0 -1 0, 0, 1, 2;" +
"-fx-background-radius: 10px, 10px, 10px, 10px;" +
"-fx-padding: 1;" +
"-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);");
popup.getContent().add(streamPreview);
StackPane.setMargin(streamPreview, new Insets(5));
createTimerThread();
}
@Override
public void handle(MouseEvent event) {
if(!isInPreviewColumn(event)) {
closeCountdown = timeForPopupClose;
return;
}
if(event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY) {
model = getModel(event);
popup.setX(event.getScreenX()+ offset);
popup.setY(event.getScreenY()+ offset);
showPopup();
openCountdown = -1;
} else if(event.getEventType() == MouseEvent.MOUSE_ENTERED) {
popup.setX(event.getScreenX()+ offset);
popup.setY(event.getScreenY()+ offset);
JavaFxModel model = getModel(event);
if(model != null) {
closeCountdown = -1;
boolean modelChanged = model != this.model;
this.model = model;
if(popup.isShowing()) {
openCountdown = -1;
if(modelChanged) {
lastModelChange = System.currentTimeMillis();
changeModel = true;
streamPreview.stop();
}
} else {
openCountdown = timeForPopupOpen;
}
}
} else if(event.getEventType() == MouseEvent.MOUSE_EXITED) {
openCountdown = -1;
closeCountdown = timeForPopupClose;
model = null;
} else if(event.getEventType() == MouseEvent.MOUSE_MOVED) {
popup.setX(event.getScreenX() + offset);
popup.setY(event.getScreenY() + offset);
}
}
private boolean isInPreviewColumn(MouseEvent event) {
@SuppressWarnings("unchecked")
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
TableView<JavaFxModel> table = row.getTableView();
double offset = 0;
double width = 0;
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
offset += width;
width = col.getWidth();
if(Objects.equals(col.getId(), "preview")) {
Point2D screenToLocal = table.screenToLocal(event.getScreenX(), event.getScreenY());
double x = screenToLocal.getX();
return x >= offset && x <= offset + width;
}
}
return false;
}
private JavaFxModel getModel(MouseEvent event) {
@SuppressWarnings("unchecked")
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
TableView<JavaFxModel> table = row.getTableView();
int rowIndex = row.getIndex();
if(rowIndex < table.getItems().size()) {
return table.getItems().get(rowIndex);
} else {
return null;
}
}
private void showPopup() {
startStream(model);
}
private void startStream(JavaFxModel model) {
Platform.runLater(() -> {
streamPreview.startStream(model);
popup.show(parent.getScene().getWindow());
});
}
private void hidePopup() {
Platform.runLater(() -> {
popup.setX(-1000);
popup.setY(-1000);
popup.hide();
streamPreview.stop();
});
}
private void createTimerThread() {
Thread timerThread = new Thread(() -> {
while(true) {
openCountdown--;
if(openCountdown == 0) {
openCountdown = -1;
if(model != null) {
showPopup();
}
}
closeCountdown--;
if(closeCountdown == 0) {
hidePopup();
closeCountdown = -1;
}
openCountdown = Math.max(openCountdown, -1);
closeCountdown = Math.max(closeCountdown, -1);
long now = System.currentTimeMillis();
long diff = (now - lastModelChange);
if(changeModel && diff > 400) {
changeModel = false;
if(model != null) {
startStream(model);
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
LOG.error("PreviewPopupTimer interrupted");
break;
}
}
});
timerThread.setDaemon(true);
timerThread.setPriority(Thread.MIN_PRIORITY);
timerThread.setName("PreviewPopupTimer");
timerThread.start();
}
}

View File

@ -3,16 +3,15 @@ package ctbrec.ui;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
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.function.Function;
import java.util.stream.Collectors;
@ -20,27 +19,36 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.autofilltextbox.AutoFillTextField;
import javafx.application.Platform;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.controls.AutoFillTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell;
@ -50,6 +58,7 @@ 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;
@ -59,9 +68,6 @@ import javafx.util.Duration;
public class RecordedModelsTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
static BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue);
private ScheduledService<List<JavaFxModel>> updateService;
private Recorder recorder;
private List<Site> sites;
@ -75,6 +81,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
Label modelLabel = new Label("Model");
AutoFillTextField model;
Button addModelButton = new Button("Record");
Button pauseAll = new Button("Pause All");
Button resumeAll = new Button("Resume All");
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
super(title);
@ -96,26 +104,44 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
scrollPane.setFitToWidth(true);
BorderPane.setMargin(scrollPane, new Insets(5));
table.setEditable(false);
table.setEditable(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
PreviewPopupHandler previewPopupHandler = new PreviewPopupHandler(table);
table.setRowFactory((tableview) -> {
TableRow<JavaFxModel> row = new TableRow<>();
row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
return row;
});
TableColumn<JavaFxModel, String> preview = new TableColumn<>("🎥");
preview.setPrefWidth(35);
preview.setCellValueFactory(cdf -> new SimpleStringProperty(""));
preview.setEditable(false);
preview.setId("preview");
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("name"));
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("displayName"));
name.setEditable(false);
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url"));
url.setPrefWidth(400);
url.setEditable(false);
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty());
online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty());
online.setCellFactory(CheckBoxTableCell.forTableColumn(online));
online.setPrefWidth(100);
online.setEditable(false);
TableColumn<JavaFxModel, Boolean> recording = new TableColumn<>("Recording");
recording.setCellValueFactory((cdf) -> cdf.getValue().getRecordingProperty());
recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty());
recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording));
recording.setPrefWidth(100);
recording.setEditable(false);
TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused");
paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty());
paused.setCellValueFactory(cdf -> cdf.getValue().getPausedProperty());
paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
paused.setPrefWidth(100);
table.getColumns().addAll(name, url, online, recording, paused);
paused.setEditable(true);
table.getColumns().addAll(preview, name, url, online, recording, paused);
table.setItems(observableModels);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
popup = createContextMenu();
@ -124,14 +150,28 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
JavaFxModel model = table.getSelectionModel().getSelectedItem();
if(model != null) {
new PlayAction(table, model).execute();
}
}
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (popup != null) {
popup.hide();
}
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
if (event.getCode() == KeyCode.DELETE) {
stopAction();
stopAction(selectedModels);
} else if (event.getCode() == KeyCode.P) {
List<JavaFxModel> pausedModels = selectedModels.stream().filter(m -> m.isSuspended()).collect(Collectors.toList());
List<JavaFxModel> runningModels = selectedModels.stream().filter(m -> !m.isSuspended()).collect(Collectors.toList());
resumeRecording(pausedModels);
pauseRecording(runningModels);
}
});
scrollPane.setContent(table);
@ -141,23 +181,65 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
ObservableList<String> suggestions = FXCollections.observableArrayList();
sites.forEach(site -> suggestions.add(site.getName()));
model = new AutoFillTextField(suggestions);
model.setPrefWidth(300);
model.setPromptText("e.g. MyFreeCams:ModelName");
model.onActionHandler(e -> addModel(e));
model.setPrefWidth(600);
model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/");
model.onActionHandler(this::addModel);
model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" +
"press ENTER to confirm a suggested site name"));
BorderPane.setMargin(addModelBox, new Insets(5));
addModelButton.setOnAction((e) -> addModel(e));
addModelBox.getChildren().addAll(modelLabel, model, addModelButton);
addModelButton.setOnAction(this::addModel);
addModelBox.getChildren().addAll(modelLabel, model, addModelButton, pauseAll, resumeAll);
HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20));
pauseAll.setOnAction(this::pauseAll);
resumeAll.setOnAction(this::resumeAll);
BorderPane root = new BorderPane();
root.setPadding(new Insets(5));
root.setTop(addModelBox);
root.setCenter(scrollPane);
setContent(root);
restoreState();
}
private void addModel(ActionEvent e) {
String input = model.getText();
if (StringUtil.isBlank(input)) {
return;
}
if (input.startsWith("http")) {
addModelByUrl(input);
} else {
addModelByName(input);
}
};
private void addModelByUrl(String url) {
for (Site site : sites) {
Model model = site.createModelFromUrl(url);
if (model != null) {
try {
recorder.startRecording(model);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The model " + model.getName() + " could not be added: " + e1.getLocalizedMessage());
alert.showAndWait();
}
return;
}
}
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Unknown URL format");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The URL you entered has an unknown format or the function does not support this site, yet");
alert.showAndWait();
}
private void addModelByName(String siteModelCombo) {
String[] parts = model.getText().trim().split(":");
if (parts.length != 2) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
@ -191,8 +273,15 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
alert.setHeaderText("Couldn't add model");
alert.setContentText("The site you entered is unknown");
alert.showAndWait();
};
}
private void pauseAll(ActionEvent evt) {
new PauseAction(getTabPane(), recorder.getModelsRecording(), recorder).execute();
}
private void resumeAll(ActionEvent evt) {
new ResumeAction(getTabPane(), recorder.getModelsRecording(), recorder).execute();
}
void initializeUpdateService() {
updateService = createUpdateService();
@ -207,6 +296,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
int index = observableModels.indexOf(updatedModel);
if (index == -1) {
observableModels.add(updatedModel);
updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> {
if (newV) {
if(!recorder.isSuspended(updatedModel)) {
pauseRecording(Collections.singletonList(updatedModel));
}
} else {
if(recorder.isSuspended(updatedModel)) {
resumeRecording(Collections.singletonList(updatedModel));
}
}
});
} else {
// make sure to update the JavaFX online property, so that the table cell is updated
JavaFxModel oldModel = observableModels.get(index);
@ -222,6 +322,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
iterator.remove();
}
}
table.sort();
});
updateService.setOnFailed((event) -> {
LOG.info("Couldn't get list of models from recorder", event.getSource().getException());
@ -243,7 +345,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
.map(m -> new JavaFxModel(m))
.peek(fxm -> {
for (Recording recording : recordings) {
if(recording.getStatus() == Recording.STATUS.RECORDING &&
if(recording.getStatus() == Recording.State.RECORDING &&
recording.getModelName().equals(fxm.getName()))
{
fxm.getRecordingProperty().set(true);
@ -292,16 +394,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private ContextMenu createContextMenu() {
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
if(selectedModel == null) {
ObservableList<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
if (selectedModels.isEmpty()) {
return null;
}
MenuItem stop = new MenuItem("Remove Model");
stop.setOnAction((e) -> stopAction());
stop.setOnAction((e) -> stopAction(selectedModels));
MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction((e) -> {
Model selected = selectedModel;
Model selected = selectedModels.get(0);
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString(selected.getUrl());
@ -309,28 +411,42 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
});
MenuItem pauseRecording = new MenuItem("Pause Recording");
pauseRecording.setOnAction((e) -> pauseRecording());
pauseRecording.setOnAction((e) -> pauseRecording(selectedModels));
MenuItem resumeRecording = new MenuItem("Resume Recording");
resumeRecording.setOnAction((e) -> resumeRecording());
resumeRecording.setOnAction((e) -> resumeRecording(selectedModels));
MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModel.getUrl()));
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction((e) -> openInPlayer(selectedModel));
openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0)));
MenuItem switchStreamSource = new MenuItem("Switch resolution");
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModel));
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModels.get(0)));
MenuItem follow = new MenuItem("Follow");
follow.setOnAction((e) -> follow(selectedModels));
ContextMenu menu = new ContextMenu(stop);
menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording);
menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource);
if (selectedModels.size() == 1) {
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
} else {
menu.getItems().addAll(resumeRecording, pauseRecording);
}
menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource, follow);
if (selectedModels.size() > 1) {
copyUrl.setDisable(true);
openInPlayer.setDisable(true);
openInBrowser.setDisable(true);
switchStreamSource.setDisable(true);
}
return menu;
}
private void follow(ObservableList<JavaFxModel> selectedModels) {
new FollowAction(getTabPane(), new ArrayList<JavaFxModel>(selectedModels)).execute();
}
private void openInPlayer(JavaFxModel selectedModel) {
table.setCursor(Cursor.WAIT);
new Thread(() -> {
Player.play(selectedModel);
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
}).start();
new PlayAction(getTabPane(), selectedModel).execute();
}
private void switchStreamSource(JavaFxModel fxModel) {
@ -370,93 +486,65 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private void showStreamSwitchErrorDialog(Throwable throwable) {
showErrorDialog(throwable, "Couldn't switch stream resolution", "Error while switching stream resolution");
}
private void showErrorDialog(Throwable throwable, String header, String msg) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't switch stream resolution");
alert.setContentText("Error while switching stream resolution: " + throwable.getLocalizedMessage());
alert.setHeaderText(header);
alert.setContentText(msg + ": " + throwable.getLocalizedMessage());
alert.showAndWait();
}
private void stopAction() {
Model selected = table.getSelectionModel().getSelectedItem().getDelegate();
if (selected != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.stopRecording(selected);
observableModels.remove(selected);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't stop recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't stop recording");
alert.setContentText("Error while stopping the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
private void stopAction(List<JavaFxModel> selectedModels) {
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
new StopRecordingAction(getTabPane(), models, recorder).execute((m) -> {
observableModels.remove(m);
});
} finally {
table.setCursor(Cursor.DEFAULT);
}
}
}.start();
}
};
private void pauseRecording() {
JavaFxModel model = table.getSelectionModel().getSelectedItem();
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
if (delegate != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.suspendRecording(delegate);
Platform.runLater(() -> model.setSuspended(true));
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't pause recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't pause recording");
alert.setContentText("Error while pausing the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
table.setCursor(Cursor.DEFAULT);
}
}
}.start();
}
private void pauseRecording(List<JavaFxModel> selectedModels) {
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
new PauseAction(getTabPane(), models, recorder).execute();
};
private void resumeRecording() {
JavaFxModel model = table.getSelectionModel().getSelectedItem();
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
if (delegate != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.resumeRecording(delegate);
Platform.runLater(() -> model.setSuspended(false));
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't resume recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't resume recording");
alert.setContentText("Error while resuming the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
table.setCursor(Cursor.DEFAULT);
private void resumeRecording(List<JavaFxModel> selectedModels) {
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
new ResumeAction(getTabPane(), models, recorder).execute();
}
public void saveState() {
if (!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0);
Config.getInstance().getSettings().recordedModelsSortColumn = col.getText();
Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString();
}
}.start();
double[] columnWidths = new double[table.getColumns().size()];
for (int i = 0; i < columnWidths.length; i++) {
columnWidths[i] = table.getColumns().get(i).getWidth();
}
Config.getInstance().getSettings().recordedModelsColumnWidths = columnWidths;
};
private void restoreState() {
String sortCol = Config.getInstance().getSettings().recordedModelsSortColumn;
if (StringUtil.isNotBlank(sortCol)) {
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
if (Objects.equals(sortCol, col.getText())) {
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordedModelsSortType));
table.getSortOrder().clear();
table.getSortOrder().add(col);
break;
}
}
}
double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths;
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
for (int i = 0; i < columnWidths.length; i++) {
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
}
}
}
}

View File

@ -1,13 +1,16 @@
package ctbrec.ui;
import static ctbrec.Recording.State.*;
import static javafx.scene.control.ButtonType.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.NoSuchFileException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@ -16,10 +19,13 @@ import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -29,10 +35,12 @@ import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.Recording.STATUS;
import ctbrec.Recording.State;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.download.MergedHlsDownload;
import ctbrec.sites.Site;
import ctbrec.ui.controls.Toast;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
@ -44,19 +52,28 @@ import javafx.scene.Cursor;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.PropertyValueFactory;
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.StackPane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import javafx.util.Duration;
@ -69,12 +86,17 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
private Recorder recorder;
@SuppressWarnings("unused")
private List<Site> sites;
private long spaceTotal = -1;
private long spaceFree = -1;
FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane();
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
ContextMenu popup;
ProgressBar spaceLeft;
Label spaceLabel;
Lock recordingsLock = new ReentrantLock();
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
super(title);
@ -98,6 +120,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
BorderPane.setMargin(scrollPane, new Insets(5));
table.setEditable(false);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
@ -131,16 +154,42 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
progress.setPrefWidth(100);
TableColumn<JavaFxRecording, String> size = new TableColumn<>("Size");
size.setCellValueFactory((cdf) -> cdf.getValue().getSizeProperty());
TableColumn<JavaFxRecording, Number> size = new TableColumn<>("Size");
size.setStyle("-fx-alignment: CENTER-RIGHT;");
size.setPrefWidth(100);
size.setCellValueFactory(cdf -> cdf.getValue().getSizeProperty());
size.setCellFactory(new Callback<TableColumn<JavaFxRecording, Number>, TableCell<JavaFxRecording, Number>>() {
@Override
public TableCell<JavaFxRecording, Number> call(TableColumn<JavaFxRecording, Number> param) {
TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
@Override
protected void updateItem(Number sizeInByte, boolean empty) {
if(empty || sizeInByte == null) {
setText(null);
setStyle(null);
} else {
setText(StringUtil.formatSize(sizeInByte));
setStyle("-fx-alignment: CENTER-RIGHT;");
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
int row = this.getTableRow().getIndex();
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
if(!rec.valueChanged() && rec.getStatus() == State.RECORDING) {
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
}
}
}
}
};
return cell;
}
});
table.getColumns().addAll(name, date, status, progress, size);
table.setItems(observableRecordings);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
Recording recording = table.getSelectionModel().getSelectedItem();
if(recording != null) {
popup = createContextMenu(recording);
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if(recordings != null && !recordings.isEmpty()) {
popup = createContextMenu(recordings);
if(!popup.getItems().isEmpty()) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
@ -152,35 +201,89 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
popup.hide();
}
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
JavaFxRecording recording = table.getSelectionModel().getSelectedItem();
table.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
Recording recording = table.getSelectionModel().getSelectedItem();
if(recording != null) {
if (event.getCode() == KeyCode.DELETE) {
delete(recording);
} else if (event.getCode() == KeyCode.ENTER) {
if(recording.getStatus() == STATUS.FINISHED) {
play(recording);
}
}
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if (recordings != null && !recordings.isEmpty()) {
if (event.getCode() == KeyCode.DELETE) {
if(recordings.size() > 1 || recordings.get(0).getStatus() == State.FINISHED) {
delete(recordings);
}
} else if (event.getCode() == KeyCode.ENTER) {
if(recordings.get(0).getStatus() == State.FINISHED) {
play(recordings.get(0));
}
}
}
});
scrollPane.setContent(table);
HBox spaceBox = new HBox(5);
Label l = new Label("Space left on device");
HBox.setMargin(l, new Insets(2, 0, 0, 0));
spaceBox.getChildren().add(l);
spaceLeft = new ProgressBar(0);
spaceLeft.setPrefSize(200, 22);
spaceLabel = new Label();
spaceLabel.setFont(Font.font(11));
StackPane stack = new StackPane(spaceLeft, spaceLabel);
spaceBox.getChildren().add(stack);
BorderPane.setMargin(spaceBox, new Insets(5));
BorderPane root = new BorderPane();
root.setPadding(new Insets(5));
root.setTop(spaceBox);
root.setCenter(scrollPane);
setContent(root);
restoreState();
}
void initializeUpdateService() {
updateService = createUpdateService();
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
updateService.setOnSucceeded((event) -> {
updateRecordingsTable();
updateFreeSpaceDisplay();
});
updateService.setOnFailed((event) -> {
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR);
autosizeAlert.setTitle("Whoopsie!");
autosizeAlert.setHeaderText("Recordings not available");
autosizeAlert.setContentText("An error occured while retrieving the list of recordings");
autosizeAlert.showAndWait();
});
}
private void updateFreeSpaceDisplay() {
if(spaceTotal != -1 && spaceFree != -1) {
double free = ((double)spaceFree) / spaceTotal;
spaceLeft.setProgress(free);
double totalGiB = ((double) spaceTotal) / 1024 / 1024 / 1024;
double freeGiB = ((double) spaceFree) / 1024 / 1024 / 1024;
DecimalFormat df = new DecimalFormat("0.00");
String tt = df.format(freeGiB) + " / " + df.format(totalGiB) + " GiB";
spaceLeft.setTooltip(new Tooltip(tt));
spaceLabel.setText(tt);
}
}
private void updateRecordingsTable() {
List<JavaFxRecording> recordings = updateService.getValue();
if (recordings == null) {
return;
}
recordingsLock.lock();
try {
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
JavaFxRecording old = iterator.next();
if (!recordings.contains(old)) {
@ -199,16 +302,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
old.update(recording);
}
}
} finally {
recordingsLock.unlock();
}
table.sort();
});
updateService.setOnFailed((event) -> {
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR);
autosizeAlert.setTitle("Whoopsie!");
autosizeAlert.setHeaderText("Recordings not available");
autosizeAlert.setContentText("An error occured while retrieving the list of recordings");
autosizeAlert.showAndWait();
});
}
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
@ -218,12 +315,27 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
return new Task<List<JavaFxRecording>>() {
@Override
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
updateSpace();
List<JavaFxRecording> recordings = new ArrayList<>();
for (Recording rec : recorder.getRecordings()) {
recordings.add(new JavaFxRecording(rec));
}
return recordings;
}
private void updateSpace() {
try {
spaceTotal = recorder.getTotalSpaceBytes();
spaceFree = recorder.getFreeSpaceBytes();
Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip()));
} catch (NoSuchFileException e) {
// recordings dir does not exist
Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip("Recordings directory does not exist")));
} catch (IOException e) {
LOG.error("Couldn't update free space", e);
}
}
};
}
};
@ -255,7 +367,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
}
private ContextMenu createContextMenu(Recording recording) {
private ContextMenu createContextMenu(List<JavaFxRecording> recordings) {
ContextMenu contextMenu = new ContextMenu();
contextMenu.setHideOnEscape(true);
contextMenu.setAutoHide(true);
@ -263,9 +375,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction((e) -> {
play(recording);
play(recordings.get(0));
});
if(recording.getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) {
if(recordings.get(0).getStatus() == State.FINISHED || Config.getInstance().getSettings().localRecording) {
contextMenu.getItems().add(openInPlayer);
}
@ -285,16 +397,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
MenuItem deleteRecording = new MenuItem("Delete");
deleteRecording.setOnAction((e) -> {
delete(recording);
delete(recordings);
});
if(recording.getStatus() == STATUS.FINISHED) {
if(recordings.get(0).getStatus() == State.FINISHED || recordings.size() > 1) {
contextMenu.getItems().add(deleteRecording);
}
MenuItem openDir = new MenuItem("Open directory");
openDir.setOnAction((e) -> {
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String path = recording.getPath();
String path = recordings.get(0).getPath();
File tsFile = new File(recordingsDir, path);
new Thread(() -> {
DesktopIntegration.open(tsFile.getParent());
@ -307,16 +419,22 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
MenuItem downloadRecording = new MenuItem("Download");
downloadRecording.setOnAction((e) -> {
try {
download(recording);
download(recordings.get(0));
} catch (IOException | ParseException | PlaylistException e1) {
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1);
LOG.error("Error while downloading recording", e1);
}
});
if (!Config.getInstance().getSettings().localRecording && recording.getStatus() == STATUS.FINISHED) {
if (!Config.getInstance().getSettings().localRecording && recordings.get(0).getStatus() == State.FINISHED) {
contextMenu.getItems().add(downloadRecording);
}
if(recordings.size() > 1) {
openInPlayer.setDisable(true);
openDir.setDisable(true);
downloadRecording.setDisable(true);
}
return contextMenu;
}
@ -346,11 +464,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
download.start(url.toString(), target, (progress) -> {
Platform.runLater(() -> {
if (progress == 100) {
recording.setStatus(STATUS.FINISHED);
recording.setStatus(FINISHED);
recording.setProgress(-1);
LOG.debug("Download finished for recording {}", recording.getPath());
} else {
recording.setStatus(STATUS.DOWNLOADING);
recording.setStatus(DOWNLOADING);
recording.setProgress(progress);
}
});
@ -365,7 +483,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
Platform.runLater(new Runnable() {
@Override
public void run() {
recording.setStatus(STATUS.FINISHED);
recording.setStatus(FINISHED);
recording.setProgress(-1);
}
});
@ -376,85 +494,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
t.setName("Download Thread " + recording.getPath());
t.start();
recording.setStatus(STATUS.DOWNLOADING);
recording.setStatus(State.DOWNLOADING);
recording.setProgress(0);
}
}
// private void download(Recording recording) throws IOException, ParseException, PlaylistException {
// String filename = recording.getPath().replaceAll("/", "-") + ".ts";
// FileChooser chooser = new FileChooser();
// chooser.setInitialFileName(filename);
// if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
// File dir = new File(config.getSettings().lastDownloadDir);
// while(!dir.exists()) {
// dir = dir.getParentFile();
// }
// chooser.setInitialDirectory(dir);
// }
// File target = chooser.showSaveDialog(null);
// if(target != null) {
// config.getSettings().lastDownloadDir = target.getParent();
// String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls";
// URL url = new URL(hlsBase + "/" + recording.getPath() + "/playlist.m3u8");
// LOG.info("Downloading {}", recording.getPath());
//
// PlaylistParser parser = new PlaylistParser(url.openStream(), Format.EXT_M3U, Encoding.UTF_8);
// Playlist playlist = parser.parse();
// MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
// List<TrackData> tracks = mediaPlaylist.getTracks();
// List<String> segmentUris = new ArrayList<>();
// for (TrackData trackData : tracks) {
// String segmentUri = hlsBase + "/" + recording.getPath() + "/" + trackData.getUri();
// segmentUris.add(segmentUri);
// }
//
// Thread t = new Thread() {
// @Override
// public void run() {
// try(FileOutputStream fos = new FileOutputStream(target)) {
// for (int i = 0; i < segmentUris.size(); i++) {
// URL segment = new URL(segmentUris.get(i));
// InputStream in = segment.openStream();
// byte[] b = new byte[1024];
// int length = -1;
// while( (length = in.read(b)) >= 0 ) {
// fos.write(b, 0, length);
// }
// in.close();
// int progress = (int) (i * 100.0 / segmentUris.size());
// Platform.runLater(new Runnable() {
// @Override
// public void run() {
// recording.setStatus(STATUS.DOWNLOADING);
// recording.setProgress(progress);
// }
// });
// }
//
// } catch (FileNotFoundException e) {
// showErrorDialog("Error while downloading recording", "The target file couldn't be created", e);
// LOG.error("Error while downloading recording", e);
// } catch (IOException e) {
// showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e);
// LOG.error("Error while downloading recording", e);
// } finally {
// Platform.runLater(new Runnable() {
// @Override
// public void run() {
// recording.setStatus(STATUS.FINISHED);
// recording.setProgress(-1);
// }
// });
// }
// }
// };
// t.setDaemon(true);
// t.setName("Download Thread " + recording.getPath());
// t.start();
// }
// }
private void showErrorDialog(final String title, final String msg, final Exception e) {
Platform.runLater(new Runnable() {
@Override
@ -474,7 +518,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
new Thread() {
@Override
public void run() {
Player.play(recording);
boolean started = Player.play(recording);
if(started && Config.getInstance().getSettings().showPlayerStarting) {
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
}
}
}.start();
} else {
@ -483,19 +530,26 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
new Thread() {
@Override
public void run() {
Player.play(url);
boolean started = Player.play(url);
if(started && Config.getInstance().getSettings().showPlayerStarting) {
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
}
}
}.start();
}
}
private void delete(Recording r) {
if(r.getStatus() != STATUS.FINISHED) {
return;
}
private void delete(List<JavaFxRecording> recordings) {
table.setCursor(Cursor.WAIT);
String msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?";
String msg;
if(recordings.size() > 1) {
msg = "Delete " + recordings.size() + " recordings for good?";
} else {
Recording r = recordings.get(0);
msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?";
}
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, YES, NO);
confirm.setTitle("Delete recording?");
confirm.setHeaderText(msg);
@ -505,14 +559,26 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
Thread deleteThread = new Thread() {
@Override
public void run() {
recordingsLock.lock();
try {
List<Recording> deleted = new ArrayList<>();
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
JavaFxRecording r = iterator.next();
if(r.getStatus() != FINISHED) {
continue;
}
try {
recorder.delete(r);
Platform.runLater(() -> observableRecordings.remove(r));
deleted.add(r);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Error while deleting recording", e1);
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
}
}
observableRecordings.removeAll(deleted);
} finally {
table.setCursor(Cursor.DEFAULT);
recordingsLock.unlock();
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
}
}
};
@ -521,4 +587,38 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
table.setCursor(Cursor.DEFAULT);
}
}
public void saveState() {
if(!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxRecording, ?> col = table.getSortOrder().get(0);
Config.getInstance().getSettings().recordingsSortColumn = col.getText();
Config.getInstance().getSettings().recordingsSortType = col.getSortType().toString();
}
double[] columnWidths = new double[table.getColumns().size()];
for (int i = 0; i < columnWidths.length; i++) {
columnWidths[i] = table.getColumns().get(i).getWidth();
}
Config.getInstance().getSettings().recordingsColumnWidths = columnWidths;
};
private void restoreState() {
String sortCol = Config.getInstance().getSettings().recordingsSortColumn;
if(StringUtil.isNotBlank(sortCol)) {
for (TableColumn<JavaFxRecording, ?> col : table.getColumns()) {
if(Objects.equals(sortCol, col.getText())) {
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordingsSortType));
table.getSortOrder().clear();
table.getSortOrder().add(col);
break;
}
}
}
double[] columnWidths = Config.getInstance().getSettings().recordingsColumnWidths;
if(columnWidths != null && columnWidths.length == table.getColumns().size()) {
for (int i = 0; i < columnWidths.length; i++) {
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
}
}
}
}

View File

@ -1,613 +0,0 @@
package ctbrec.ui;
import static ctbrec.Settings.DirectoryStructure.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.Settings;
import ctbrec.Settings.DirectoryStructure;
import ctbrec.sites.ConfigUI;
import ctbrec.sites.Site;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Accordion;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;;
public class SettingsTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
public static final int CHECKBOX_MARGIN = 6;
private TextField recordingsDirectory;
private Button recordingsDirectoryButton;
private Button postProcessingDirectoryButton;
private TextField mediaPlayer;
private TextField postProcessing;
private TextField server;
private TextField port;
private CheckBox loadResolution;
private CheckBox secureCommunication = new CheckBox();
private CheckBox chooseStreamQuality = new CheckBox();
private CheckBox multiplePlayers = new CheckBox();
private RadioButton recordLocal;
private RadioButton recordRemote;
private ToggleGroup recordLocation;
private ProxySettingsPane proxySettingsPane;
private ComboBox<Integer> maxResolution;
private ComboBox<SplitAfterOption> splitAfter;
private ComboBox<DirectoryStructure> directoryStructure;
private List<Site> sites;
private Label restartLabel;
private Accordion credentialsAccordion = new Accordion();
public SettingsTab(List<Site> sites) {
this.sites = sites;
setText("Settings");
createGui();
setClosable(false);
setRecordingMode(recordLocal.isSelected());
}
private void createGui() {
// set up main layout, 2 columns with VBoxes 50/50
GridPane mainLayout = createGridLayout();
mainLayout.setHgap(15);
mainLayout.setVgap(15);
mainLayout.setPadding(new Insets(15));
ColumnConstraints cc = new ColumnConstraints();
cc.setPercentWidth(50);
mainLayout.getColumnConstraints().setAll(cc, cc);
setContent(new ScrollPane(mainLayout));
VBox leftSide = new VBox(15);
VBox rightSide = new VBox(15);
GridPane.setHgrow(leftSide, Priority.ALWAYS);
GridPane.setHgrow(rightSide, Priority.ALWAYS);
GridPane.setFillWidth(leftSide, true);
GridPane.setFillWidth(rightSide, true);
mainLayout.add(leftSide, 0, 1);
mainLayout.add(rightSide, 1, 1);
// restart info label
restartLabel = new Label("A restart is required to apply changes you made!");
restartLabel.setVisible(false);
restartLabel.setFont(Font.font(24));
restartLabel.setTextFill(Color.RED);
mainLayout.add(restartLabel, 0, 0);
GridPane.setColumnSpan(restartLabel, 2);
GridPane.setHalignment(restartLabel, HPos.CENTER);
// left side
leftSide.getChildren().add(createGeneralPanel());
leftSide.getChildren().add(createLocationsPanel());
leftSide.getChildren().add(createRecordLocationPanel());
proxySettingsPane = new ProxySettingsPane(this);
leftSide.getChildren().add(proxySettingsPane);
//right side
rightSide.getChildren().add(createSiteSelectionPanel());
rightSide.getChildren().add(credentialsAccordion);
for (int i = 0; i < sites.size(); i++) {
Site site = sites.get(i);
ConfigUI siteConfig = SiteUiFactory.getUi(site).getConfigUI();
if(siteConfig != null) {
TitledPane pane = new TitledPane(site.getName(), siteConfig.createConfigPanel());
credentialsAccordion.getPanes().add(pane);
}
}
credentialsAccordion.setExpandedPane(credentialsAccordion.getPanes().get(0));
}
private Node createSiteSelectionPanel() {
Settings settings = Config.getInstance().getSettings();
GridPane layout = createGridLayout();
int row = 0;
for (Site site : sites) {
Label l = new Label(site.getName());
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(site.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(site.getName());
} else {
settings.disabledSites.add(site.getName());
}
showRestartRequired();
});
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(enabled, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
}
TitledPane siteSelection = new TitledPane("Enabled Sites", layout);
siteSelection.setCollapsible(false);
return siteSelection;
}
private Node createRecordLocationPanel() {
GridPane layout = createGridLayout();
Label l = new Label("Record Location");
layout.add(l, 0, 0);
recordLocation = new ToggleGroup();
recordLocal = new RadioButton("Local");
recordRemote = new RadioButton("Remote");
recordLocal.setToggleGroup(recordLocation);
recordRemote.setToggleGroup(recordLocation);
recordLocal.setSelected(Config.getInstance().getSettings().localRecording);
recordRemote.setSelected(!recordLocal.isSelected());
layout.add(recordLocal, 1, 0);
layout.add(recordRemote, 2, 0);
recordLocation.selectedToggleProperty().addListener((e) -> {
Config.getInstance().getSettings().localRecording = recordLocal.isSelected();
setRecordingMode(recordLocal.isSelected());
showRestartRequired();
});
GridPane.setMargin(l, new Insets(0, 0, CHECKBOX_MARGIN, 0));
GridPane.setMargin(recordLocal, new Insets(0, 0, CHECKBOX_MARGIN, 0));
GridPane.setMargin(recordRemote, new Insets(0, 0, CHECKBOX_MARGIN, 0));
layout.add(new Label("Server"), 0, 1);
server = new TextField(Config.getInstance().getSettings().httpServer);
server.focusedProperty().addListener((e) -> {
if(!server.getText().isEmpty()) {
Config.getInstance().getSettings().httpServer = server.getText();
}
});
GridPane.setFillWidth(server, true);
GridPane.setHgrow(server, Priority.ALWAYS);
GridPane.setColumnSpan(server, 2);
layout.add(server, 1, 1);
layout.add(new Label("Port"), 0, 2);
port = new TextField(Integer.toString(Config.getInstance().getSettings().httpPort));
port.focusedProperty().addListener((e) -> {
if(!port.getText().isEmpty()) {
try {
Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText());
port.setBorder(Border.EMPTY);
port.setTooltip(null);
} catch (NumberFormatException e1) {
port.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2))));
port.setTooltip(new Tooltip("Port has to be a number in the range 1 - 65536"));
}
}
});
GridPane.setFillWidth(port, true);
GridPane.setHgrow(port, Priority.ALWAYS);
GridPane.setColumnSpan(port, 2);
layout.add(port, 1, 2);
l = new Label("Require authentication");
layout.add(l, 0, 3);
secureCommunication.setSelected(Config.getInstance().getSettings().requireAuthentication);
secureCommunication.setOnAction((e) -> {
Config.getInstance().getSettings().requireAuthentication = secureCommunication.isSelected();
if(secureCommunication.isSelected()) {
byte[] key = Config.getInstance().getSettings().key;
if(key == null) {
key = Hmac.generateKey();
Config.getInstance().getSettings().key = key;
}
TextInputDialog keyDialog = new TextInputDialog();
keyDialog.setResizable(true);
keyDialog.setTitle("Server Authentication");
keyDialog.setHeaderText("A key has been generated");
keyDialog.setContentText("Add this setting to your server's config.json:\n");
keyDialog.getEditor().setText("\"key\": " + Arrays.toString(key));
keyDialog.getEditor().setEditable(false);
keyDialog.setWidth(800);
keyDialog.setHeight(200);
keyDialog.show();
}
});
GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0));
GridPane.setMargin(secureCommunication, new Insets(4, 0, 0, 0));
layout.add(secureCommunication, 1, 3);
TitledPane recordLocation = new TitledPane("Record Location", layout);
recordLocation.setCollapsible(false);
return recordLocation;
}
private Node createLocationsPanel() {
int row = 0;
GridPane layout = createGridLayout();
layout.add(new Label("Recordings Directory"), 0, row);
recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir);
recordingsDirectory.focusedProperty().addListener(createRecordingsDirectoryFocusListener());
recordingsDirectory.setPrefWidth(400);
GridPane.setFillWidth(recordingsDirectory, true);
GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS);
GridPane.setColumnSpan(recordingsDirectory, 2);
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(recordingsDirectory, 1, row);
recordingsDirectoryButton = createRecordingsBrowseButton();
layout.add(recordingsDirectoryButton, 3, row++);
layout.add(new Label("Directory Structure"), 0, row);
List<DirectoryStructure> options = new ArrayList<>();
options.add(FLAT);
options.add(ONE_PER_MODEL);
options.add(ONE_PER_RECORDING);
directoryStructure = new ComboBox<>(FXCollections.observableList(options));
directoryStructure.setValue(Config.getInstance().getSettings().recordingsDirStructure);
directoryStructure.setOnAction((evt) -> Config.getInstance().getSettings().recordingsDirStructure = directoryStructure.getValue());
GridPane.setColumnSpan(directoryStructure, 2);
GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(directoryStructure, 1, row++);
layout.add(new Label("Post-Processing"), 0, row);
postProcessing = new TextField(Config.getInstance().getSettings().postProcessing);
postProcessing.focusedProperty().addListener(createPostProcessingFocusListener());
GridPane.setFillWidth(postProcessing, true);
GridPane.setHgrow(postProcessing, Priority.ALWAYS);
GridPane.setColumnSpan(postProcessing, 2);
GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(postProcessing, 1, row);
postProcessingDirectoryButton = createPostProcessingBrowseButton();
layout.add(postProcessingDirectoryButton, 3, row++);
layout.add(new Label("Player"), 0, row);
mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer);
mediaPlayer.focusedProperty().addListener(createMpvFocusListener());
GridPane.setFillWidth(mediaPlayer, true);
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
GridPane.setColumnSpan(mediaPlayer, 2);
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(mediaPlayer, 1, row);
layout.add(createMpvBrowseButton(), 3, row++);
TitledPane locations = new TitledPane("Locations", layout);
locations.setCollapsible(false);
return locations;
}
private Node createGeneralPanel() {
GridPane layout = createGridLayout();
int row = 0;
Label l = new Label("Display stream resolution in overview");
layout.add(l, 0, row);
loadResolution = new CheckBox();
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
loadResolution.setOnAction((e) -> {
Config.getInstance().getSettings().determineResolution = loadResolution.isSelected();
if(!loadResolution.isSelected()) {
ThumbOverviewTab.queue.clear();
}
});
//GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(loadResolution, 1, row++);
l = new Label("Allow multiple players");
layout.add(l, 0, row);
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(multiplePlayers, 1, row++);
l = new Label("Manually select stream quality");
layout.add(l, 0, row);
chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality);
chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected());
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(chooseStreamQuality, 1, row++);
l = new Label("Maximum resolution (0 = unlimited)");
layout.add(l, 0, row);
List<Integer> resolutionOptions = new ArrayList<>();
resolutionOptions.add(1080);
resolutionOptions.add(720);
resolutionOptions.add(600);
resolutionOptions.add(480);
resolutionOptions.add(0);
maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions));
setMaxResolutionValue();
maxResolution.setOnAction((e) -> Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem());
layout.add(maxResolution, 1, row++);
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
l = new Label("Split recordings after (minutes)");
layout.add(l, 0, row);
List<SplitAfterOption> options = new ArrayList<>();
options.add(new SplitAfterOption("disabled", 0));
options.add(new SplitAfterOption("10 min", 10 * 60));
options.add(new SplitAfterOption("15 min", 15 * 60));
options.add(new SplitAfterOption("20 min", 20 * 60));
options.add(new SplitAfterOption("30 min", 30 * 60));
options.add(new SplitAfterOption("60 min", 60 * 60));
splitAfter = new ComboBox<>(FXCollections.observableList(options));
layout.add(splitAfter, 1, row++);
setSplitAfterValue();
splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue());
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
maxResolution.prefWidthProperty().bind(splitAfter.widthProperty());
TitledPane general = new TitledPane("General", layout);
general.setCollapsible(false);
return general;
}
private void setSplitAfterValue() {
int value = Config.getInstance().getSettings().splitRecordings;
for (SplitAfterOption option : splitAfter.getItems()) {
if(option.getValue() == value) {
splitAfter.getSelectionModel().select(option);
}
}
}
private void setMaxResolutionValue() {
int value = Config.getInstance().getSettings().maximumResolution;
for (Integer option : maxResolution.getItems()) {
if(option == value) {
maxResolution.getSelectionModel().select(option);
}
}
}
void showRestartRequired() {
restartLabel.setVisible(true);
}
public static GridPane createGridLayout() {
GridPane layout = new GridPane();
layout.setPadding(new Insets(10));
layout.setHgap(5);
layout.setVgap(5);
return layout;
}
private void setRecordingMode(boolean local) {
server.setDisable(local);
port.setDisable(local);
secureCommunication.setDisable(local);
recordingsDirectory.setDisable(!local);
recordingsDirectoryButton.setDisable(!local);
splitAfter.setDisable(!local);
maxResolution.setDisable(!local);
postProcessing.setDisable(!local);
postProcessingDirectoryButton.setDisable(!local);
directoryStructure.setDisable(!local);
}
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
return new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue, Boolean newPropertyValue) {
if (newPropertyValue) {
recordingsDirectory.setBorder(Border.EMPTY);
recordingsDirectory.setTooltip(null);
} else {
String input = recordingsDirectory.getText();
File newDir = new File(input);
setRecordingsDir(newDir);
}
}
};
}
private ChangeListener<? super Boolean> createMpvFocusListener() {
return new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue, Boolean newPropertyValue) {
if (newPropertyValue) {
mediaPlayer.setBorder(Border.EMPTY);
mediaPlayer.setTooltip(null);
} else {
String input = mediaPlayer.getText();
File program = new File(input);
setMpv(program);
}
}
};
}
private ChangeListener<? super Boolean> createPostProcessingFocusListener() {
return new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue, Boolean newPropertyValue) {
if (newPropertyValue) {
postProcessing.setBorder(Border.EMPTY);
postProcessing.setTooltip(null);
} else {
String input = postProcessing.getText();
if(!input.trim().isEmpty()) {
File program = new File(input);
setPostProcessing(program);
} else {
Config.getInstance().getSettings().postProcessing = "";
}
}
}
};
}
private void setMpv(File program) {
String msg = validateProgram(program);
if (msg != null) {
mediaPlayer.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2))));
mediaPlayer.setTooltip(new Tooltip(msg));
} else {
Config.getInstance().getSettings().mediaPlayer = mediaPlayer.getText();
}
}
private void setPostProcessing(File program) {
String msg = validateProgram(program);
if (msg != null) {
postProcessing.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2))));
postProcessing.setTooltip(new Tooltip(msg));
} else {
Config.getInstance().getSettings().postProcessing = postProcessing.getText();
}
}
private String validateProgram(File program) {
if (program == null || !program.exists()) {
return "File does not exist";
} else if (!program.isFile() || !program.canExecute()) {
return "This is not an executable application";
}
return null;
}
private Button createRecordingsBrowseButton() {
Button button = new Button("Select");
button.setOnAction((e) -> {
DirectoryChooser chooser = new DirectoryChooser();
File currentDir = new File(Config.getInstance().getSettings().recordingsDir);
if (currentDir.exists() && currentDir.isDirectory()) {
chooser.setInitialDirectory(currentDir);
}
File selectedDir = chooser.showDialog(null);
if(selectedDir != null) {
setRecordingsDir(selectedDir);
}
});
return button;
}
private Node createMpvBrowseButton() {
Button button = new Button("Select");
button.setOnAction((e) -> {
FileChooser chooser = new FileChooser();
File program = chooser.showOpenDialog(null);
if(program != null) {
try {
mediaPlayer.setText(program.getCanonicalPath());
} catch (IOException e1) {
LOG.error("Couldn't determine path", e1);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't determine path");
alert.showAndWait();
}
setMpv(program);
}
});
return button;
}
private Button createPostProcessingBrowseButton() {
Button button = new Button("Select");
button.setOnAction((e) -> {
FileChooser chooser = new FileChooser();
File program = chooser.showOpenDialog(null);
if(program != null) {
try {
postProcessing.setText(program.getCanonicalPath());
} catch (IOException e1) {
LOG.error("Couldn't determine path", e1);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't determine path");
alert.showAndWait();
}
setPostProcessing(program);
}
});
return button;
}
private void setRecordingsDir(File dir) {
if (dir != null && dir.isDirectory()) {
try {
String path = dir.getCanonicalPath();
Config.getInstance().getSettings().recordingsDir = path;
recordingsDirectory.setText(path);
} catch (IOException e1) {
LOG.error("Couldn't determine directory path", e1);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't determine directory path");
alert.showAndWait();
}
} else {
recordingsDirectory.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2))));
if (!dir.isDirectory()) {
recordingsDirectory.setTooltip(new Tooltip("This is not a directory"));
}
if (!dir.exists()) {
recordingsDirectory.setTooltip(new Tooltip("Directory does not exist"));
}
}
}
@Override
public void selected() {
}
@Override
public void deselected() {
saveConfig();
}
public void saveConfig() {
proxySettingsPane.saveConfig();
}
public static class SplitAfterOption {
private String label;
private int value;
public SplitAfterOption(String label, int value) {
super();
this.label = label;
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return label;
}
}
}

View File

@ -1,40 +1,18 @@
package ctbrec.ui;
import ctbrec.sites.Site;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Tab;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
public class SiteTab extends Tab implements TabSelectionListener {
private BorderPane rootPane = new BorderPane();
private HBox tokenPanel;
private SiteTabPane siteTabPane;
public SiteTab(Site site, Scene scene) {
super(site.getName());
setClosable(false);
setContent(rootPane);
siteTabPane = new SiteTabPane(site, scene);
rootPane.setCenter(siteTabPane);
if (site.supportsTips() && site.credentialsAvailable()) {
Button buyTokens = new Button("Buy Tokens");
buyTokens.setOnAction((e) -> DesktopIntegration.open(site.getBuyTokensLink()));
TokenLabel tokenBalance = new TokenLabel(site);
tokenPanel = new HBox(5, tokenBalance, buyTokens);
tokenPanel.setAlignment(Pos.BASELINE_RIGHT);
rootPane.setTop(tokenPanel);
// HBox.setMargin(tokenBalance, new Insets(0, 5, 0, 0));
// HBox.setMargin(buyTokens, new Insets(0, 5, 0, 0));
tokenBalance.loadBalance();
BorderPane.setMargin(tokenPanel, new Insets(5, 10, 0, 10));
}
setContent(siteTabPane);
}
@Override

View File

@ -7,12 +7,14 @@ import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
import ctbrec.ui.sites.cam4.Cam4SiteUi;
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
import ctbrec.ui.sites.fc2live.Fc2LiveUi;
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
import ctbrec.ui.sites.streamate.StreamateSiteUi;
public class SiteUiFactory {
@ -22,8 +24,9 @@ public class SiteUiFactory {
private static ChaturbateSiteUi ctbSiteUi;
private static Fc2LiveUi fc2SiteUi;
private static MyFreeCamsSiteUi mfcSiteUi;
private static StreamateSiteUi streamateSiteUi;
public static SiteUI getUi(Site site) {
public static synchronized SiteUI getUi(Site site) {
if (site instanceof BongaCams) {
if (bongaSiteUi == null) {
bongaSiteUi = new BongaCamsSiteUi((BongaCams) site);
@ -54,6 +57,11 @@ public class SiteUiFactory {
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);
}
return mfcSiteUi;
} else if (site instanceof Streamate) {
if (streamateSiteUi == null) {
streamateSiteUi = new StreamateSiteUi((Streamate) site);
}
return streamateSiteUi;
}
throw new RuntimeException("Unknown site " + site.getName());
}

View File

@ -1,5 +1,6 @@
package ctbrec.ui;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@ -15,14 +16,17 @@ public class StreamSourceSelectionDialog {
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
@Override
protected List<StreamSource> call() throws Exception {
return model.getStreamSources();
List<StreamSource> sources = model.getStreamSources();
Collections.sort(sources);
return sources;
}
};
selectStreamSource.setOnSucceeded((e) -> {
List<StreamSource> sources;
try {
sources = selectStreamSource.get();
ChoiceDialog<StreamSource> choiceDialog = new ChoiceDialog<StreamSource>(sources.get(sources.size()-1), sources);
int selectedIndex = model.getStreamUrlIndex() > -1 ? Math.min(model.getStreamUrlIndex(), sources.size()-1) : sources.size()-1;
ChoiceDialog<StreamSource> choiceDialog = new ChoiceDialog<StreamSource>(sources.get(selectedIndex), sources);
choiceDialog.setTitle("Stream Quality");
choiceDialog.setHeaderText("Select your preferred stream quality");
choiceDialog.setResizable(true);

View File

@ -5,16 +5,24 @@ import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.iheartradio.m3u8.ParseException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.recorder.Recorder;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.controls.StreamPreview;
import javafx.animation.FadeTransition;
import javafx.animation.FillTransition;
import javafx.animation.ParallelTransition;
@ -36,12 +44,15 @@ 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 {
@ -49,6 +60,7 @@ public class ThumbCell extends StackPane {
private static final Duration ANIMATION_DURATION = new Duration(250);
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);
@ -73,14 +85,26 @@ public class ThumbCell extends StackPane {
private ObservableList<Node> thumbCellList;
private boolean mouseHovering = false;
private boolean recording = false;
private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(30);
private static Cache<Model, int[]> resolutionCache = CacheBuilder.newBuilder()
.expireAfterAccess(4, TimeUnit.HOURS)
.maximumSize(1000)
.build();
private ThumbOverviewTab parent;
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) {
this.parent = parent;
this.thumbCellList = parent.grid.getChildren();
this.model = model;
this.recorder = recorder;
recording = recorder.isRecording(model);
model.setSuspended(recorder.isSuspended(model));
this.setStyle("-fx-background-color: lightgray");
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);
@ -109,7 +133,7 @@ public class ThumbCell extends StackPane {
StackPane.setMargin(resolutionBackground, new Insets(2));
getChildren().add(resolutionBackground);
name = new Text(model.getName());
name = new Text(model.getDisplayName());
name.setFill(Color.WHITE);
name.setFont(new Font("Sansserif", 16));
name.setTextAlignment(TextAlignment.CENTER);
@ -150,8 +174,14 @@ public class ThumbCell extends StackPane {
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
getChildren().add(pausedIndicator);
if(Config.getInstance().getSettings().previewInThumbnails) {
getChildren().add(createPreviewTrigger());
}
selectionOverlay = new Rectangle();
selectionOverlay.setOpacity(0);
selectionOverlay.visibleProperty().bind(selectionProperty);
selectionOverlay.widthProperty().bind(widthProperty());
selectionOverlay.heightProperty().bind(heightProperty());
StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT);
getChildren().add(selectionOverlay);
@ -178,9 +208,51 @@ public class ThumbCell extends StackPane {
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
setRecording(recording);
if(Config.getInstance().getSettings().determineResolution) {
determineResolution();
update();
}
private Node createPreviewTrigger() {
int s = 32;
StackPane previewTrigger = new StackPane();
previewTrigger.setStyle("-fx-background-color: white;");
previewTrigger.setOpacity(.8);
previewTrigger.setMaxSize(s, s);
Polygon play = new Polygon(new double[] {
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);
Circle clip = new Circle(s / 2);
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 -> setPreviewVisible(previewTrigger, true));
previewTrigger.setOnMouseExited(evt -> setPreviewVisible(previewTrigger, false));
return previewTrigger;
}
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);
streamPreview.startStream(model);
recordingIndicator.setVisible(!visible);
pausedIndicator.setVisible(!visible);
if(!visible) {
updateRecordingIndicator();
}
previewTrigger.setCursor(visible ? Cursor.HAND : Cursor.DEFAULT);
}
public void setSelected(boolean selected) {
@ -203,21 +275,32 @@ public class ThumbCell extends StackPane {
return;
}
int[] resolution = resolutionCache.getIfPresent(model);
if(resolution != null) {
ThumbOverviewTab.threadPool.submit(() -> {
try {
updateResolutionTag(resolution);
} catch(Exception e) {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
}
});
} else {
ThumbOverviewTab.threadPool.submit(() -> {
try {
ThumbOverviewTab.resolutionProcessing.add(model);
int[] resolution = model.getStreamResolution(false);
updateResolutionTag(resolution);
int[] _resolution = model.getStreamResolution(false);
resolutionCache.put(model, _resolution);
updateResolutionTag(_resolution);
// the model is online, but the resolution is 0. probably something went wrong
// when we first requested the stream info, so we remove this invalid value from the "cache"
// so that it is requested again
if (model.isOnline() && resolution[1] == 0) {
if (model.isOnline() && _resolution[1] == 0) {
LOG.trace("Removing invalid resolution value for {}", model.getName());
model.invalidateCacheEntries();
}
Thread.sleep(500);
Thread.sleep(100);
} catch (IOException | InterruptedException e1) {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
} catch(ExecutionException e) {
@ -233,18 +316,19 @@ public class ThumbCell extends StackPane {
}
});
}
}
private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException, InterruptedException {
String _res = "n/a";
Paint resolutionBackgroundColor = resolutionOnlineColor;
String state = model.getOnlineState(false);
String state = model.getOnlineState(false).toString();
if (model.isOnline()) {
LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]);
LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size());
final int w = resolution[1];
_res = w > 0 ? w != Integer.MAX_VALUE ? Integer.toString(w) : "HD" : state;
} else {
_res = model.getOnlineState(false);
_res = model.getOnlineState(false).toString();
resolutionBackgroundColor = resolutionOfflineColor;
}
final String resText = _res;
@ -262,21 +346,41 @@ public class ThumbCell extends StackPane {
private void setImage(String url) {
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
Image img = new Image(url, true);
// wait for the image to load, otherwise the ImageView replaces the current image with an "empty" image,
// which causes to show the grey background until the image is loaded
boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails;
if(updateThumbs || iv.getImage() == null) {
imageLoadingThreadPool.submit(() -> {
Request req = new Request.Builder()
.url(url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.build();
try(Response resp = CamrecApplication.httpClient.execute(req)) {
if(resp.isSuccessful()) {
Image img = new Image(resp.body().byteStream());
if(img.progressProperty().get() == 1.0) {
Platform.runLater(() -> {
iv.setImage(img);
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
});
} else {
img.progressProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if(newValue.doubleValue() == 1.0) {
//imgAspectRatio = img.getHeight() / img.getWidth();
iv.setImage(img);
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
}
}
});
}
} else {
throw new HttpException(resp.code(), resp.message());
}
} catch (IOException e) {
LOG.error("Error loading image", e);
}
});
}
}
}
Image getImage() {
@ -297,11 +401,7 @@ public class ThumbCell extends StackPane {
}
void startPlayer() {
setCursor(Cursor.WAIT);
new Thread(() -> {
Player.play(model);
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
}).start();
new PlayAction(this, model).execute();
}
private void setRecording(boolean recording) {
@ -314,6 +414,10 @@ public class ThumbCell extends StackPane {
nameBackground.setFill(c);
}
updateRecordingIndicator();
}
private void updateRecordingIndicator() {
if(recording) {
recordingIndicator.setVisible(!model.isSuspended());
pausedIndicator.setVisible(model.isSuspended());
@ -458,7 +562,7 @@ public class ThumbCell extends StackPane {
this.model.setPreview(model.getPreview());
this.model.setTags(model.getTags());
this.model.setUrl(model.getUrl());
this.model.setSuspended(model.isSuspended());
this.model.setSuspended(recorder.isSuspended(model));
update();
}
@ -475,7 +579,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) {
@ -519,20 +623,31 @@ public class ThumbCell extends StackPane {
}
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(getHeight()-nameBackground.getHeight());
topicBackground.setHeight(h - nameBackground.getHeight());
topic.prefHeight(getHeight()-25);
topic.maxHeight(getHeight()-25);
int margin = 4;
topic.maxWidth(w-margin*2);
topic.setWrappingWidth(w-margin*2);
selectionOverlay.setWidth(w);
selectionOverlay.setHeight(getHeight());
streamPreview.resizeTo(w, h);
Rectangle clip = new Rectangle(w, h);
clip.setArcWidth(10);
clip.arcHeightProperty().bind(clip.arcWidthProperty());
this.setClip(clip);
}
}

View File

@ -1,5 +1,6 @@
package ctbrec.ui;
import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException;
import java.net.SocketTimeoutException;
@ -26,18 +27,25 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.event.EventBusHolder;
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;
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;
@ -60,12 +68,16 @@ 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;
import javafx.util.Duration;
@ -94,6 +106,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;
@ -111,10 +126,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();
@ -123,12 +138,49 @@ 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(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();
}
});
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(450);
popover.pushPage(popoverTreelist);
StackPane.setAlignment(popover, Pos.TOP_RIGHT);
StackPane.setMargin(popover, new Insets(35, 50, 0, 0));
HBox topBar = new HBox(5);
HBox.setHgrow(filterInput, Priority.ALWAYS);
topBar.getChildren().add(filterInput);
if (site.supportsTips() && site.credentialsAvailable()) {
Button buyTokens = new Button("Buy Tokens");
buyTokens.setOnAction((e) -> DesktopIntegration.open(site.getBuyTokensLink()));
TokenLabel tokenBalance = new TokenLabel(site);
tokenBalance.setAlignment(Pos.CENTER_RIGHT);
tokenBalance.prefHeightProperty().bind(buyTokens.heightProperty());
topBar.getChildren().addAll(tokenBalance, buyTokens);
tokenBalance.loadBalance();
}
if(site.supportsSearch()) {
topBar.getChildren().add(searchInput);
}
BorderPane.setMargin(topBar, new Insets(0, 5, 0, 5));
scrollPane.setContent(grid);
scrollPane.setFitToHeight(true);
@ -184,14 +236,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));
@ -242,7 +349,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
}
List<Model> models = updateService.getValue();
updateGrid(models);
}
protected void updateGrid(List<? extends Model> models) {
@ -371,20 +477,13 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
Map<String, Object> event = new HashMap<>();
event.put("event", "tokens.sent");
event.put("amount", tokens);
CamrecApplication.bus.post(event);
EventBusHolder.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();
LOG.error("An error occured while sending tip", e1);
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);
}
}
});
@ -468,7 +567,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
translate.setFromX(0);
translate.setFromY(0);
translate.setByX(-tx.getTx() - 200);
translate.setByY(-offsetInViewPort + getFollowedTabYPosition());
TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider();
Tab followedTab = tabProvider.getFollowedTab();
translate.setByY(-offsetInViewPort + getFollowedTabYPosition(followedTab));
StackPane.setMargin(iv, new Insets(offsetInViewPort, 0, 0, tx.getTx()));
translate.setInterpolator(Interpolator.EASE_BOTH);
FadeTransition fade = new FadeTransition(Duration.millis(duration), iv);
@ -482,14 +583,42 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
pt.setOnFinished((evt) -> {
root.getChildren().remove(iv);
});
String normalStyle = followedTab.getStyle();
Color normal = Color.web("#f4f4f4");
Color highlight = Color.web("#2b8513");
Transition blink = new Transition() {
{
setCycleDuration(Duration.millis(500));
}
@Override
protected void interpolate(double frac) {
double rh = highlight.getRed();
double rn = normal.getRed();
double diff = rh - rn;
double r = (rn + diff * frac) * 255;
double gh = highlight.getGreen();
double gn = normal.getGreen();
diff = gh - gn;
double g = (gn + diff * frac) * 255;
double bh = highlight.getBlue();
double bn = normal.getBlue();
diff = bh - bn;
double b = (bn + diff * frac) * 255;
String style = "-fx-background-color: rgb(" + r + "," + g + "," + b + ")";
followedTab.setStyle(style);
}
};
blink.setCycleCount(6);
blink.setAutoReverse(true);
blink.setOnFinished((evt) -> followedTab.setStyle(normalStyle));
blink.play();
});
}
private double getFollowedTabYPosition() {
TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider();
Tab followedTab = tabProvider.getFollowedTab();
private double getFollowedTabYPosition(Tab followedTab) {
TabPane tabPane = getTabPane();
int idx = tabPane.getTabs().indexOf(followedTab);
int idx = Math.max(0, tabPane.getTabs().indexOf(followedTab));
for (Node node : tabPane.getChildrenUnmodifiable()) {
Parent p = (Parent) node;
for (Node child : p.getChildrenUnmodifiable()) {
@ -633,6 +762,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
String[] tokens = filter.split(" ");
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
searchTextBuilder.append(' ');
searchTextBuilder.append(m.getDisplayName());
searchTextBuilder.append(' ');
for (String tag : m.getTags()) {
searchTextBuilder.append(tag).append(' ');
}
@ -652,7 +783,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
tokensMissing = true;
}
} else if(token.equals("public")) {
if(!m.getOnlineState(true).equals(token)) {
if(!m.getOnlineState(true).toString().equals(token)) {
tokensMissing = true;
}
} else if(!searchText.toLowerCase().contains(token.toLowerCase())) {
@ -668,6 +799,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
public void setRecorder(Recorder recorder) {
this.recorder = recorder;
popoverTreelist.setRecorder(recorder);
}
@Override

View File

@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.sites.Site;
import ctbrec.sites.chaturbate.Chaturbate;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.control.Alert;
@ -48,7 +47,7 @@ public class TipDialog extends TextInputDialog {
int tokens = get();
Platform.runLater(() -> {
if (tokens <= 0) {
String msg = "Do you want to buy tokens now?\n\nIf you agree, Chaturbate will open in a browser. "
String msg = "Do you want to buy tokens now?\n\nIf you agree, "+site.getName()+" will open in a browser. "
+ "The used address is an affiliate link, which supports me, but doesn't cost you anything more.";
Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, ButtonType.NO, ButtonType.YES);
buyTokens.setTitle("No tokens");
@ -56,7 +55,7 @@ public class TipDialog extends TextInputDialog {
buyTokens.showAndWait();
TipDialog.this.close();
if(buyTokens.getResult() == ButtonType.YES) {
DesktopIntegration.open(Chaturbate.AFFILIATE_LINK);
DesktopIntegration.open(site.getAffiliateLink());
}
} else {
getEditor().setDisable(false);

View File

@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import ctbrec.event.EventBusHolder;
import ctbrec.sites.Site;
import javafx.application.Platform;
import javafx.concurrent.Task;
@ -24,7 +25,7 @@ public class TokenLabel extends Label {
public TokenLabel(Site site) {
this.site = site;
setText("Tokens: loading…");
CamrecApplication.bus.register(new Object() {
EventBusHolder.BUS.register(new Object() {
@Subscribe
public void tokensUpdates(Map<String, Object> e) {
if (Objects.equals("tokens", e.get("event"))) {

View File

@ -3,6 +3,7 @@ package ctbrec.ui;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.ui.CamrecApplication.Release;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -36,6 +37,7 @@ public class UpdateTab extends Tab {
try {
WebEngine webEngine = browser.getEngine();
webEngine.load("https://raw.githubusercontent.com/0xboobface/ctbrec/master/CHANGELOG.md");
webEngine.setUserDataDirectory(Config.getInstance().getConfigDir());
vbox.getChildren().add(browser);
VBox.setVgrow(browser, Priority.ALWAYS);
} catch (Exception e) {

View File

@ -1,15 +1,31 @@
package ctbrec.ui;
import java.io.File;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.OS;
import ctbrec.ui.controls.Dialogs;
import javafx.scene.control.Tab;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
public class WebbrowserTab extends Tab {
private static final transient Logger LOG = LoggerFactory.getLogger(WebbrowserTab.class);
public WebbrowserTab(String uri) {
WebView browser = new WebView();
WebEngine webEngine = browser.getEngine();
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
webEngine.setJavaScriptEnabled(true);
webEngine.load(uri);
setContent(browser);
webEngine.setOnError(evt -> {
LOG.error("Couldn't load {}", uri, evt.getException());
Dialogs.showError("Error", "Couldn't load " + uri, evt.getException());
});
}
}

View File

@ -0,0 +1,29 @@
package ctbrec.ui.action;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Node;
public class FollowAction extends ModelMassEditAction {
private static final transient Logger LOG = LoggerFactory.getLogger(FollowAction.class);
public FollowAction(Node source, List<? extends Model> models) {
super(source, models);
action = (m) -> {
try {
m.follow();
} catch(Exception e) {
LOG.error("Couldn't follow model {}", m, e);
Platform.runLater(() ->
Dialogs.showError("Couldn't follow model", "Following " + m.getName() + " failed: " + e.getMessage(), e));
}
};
}
}

View File

@ -0,0 +1,52 @@
package ctbrec.ui.action;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import ctbrec.Model;
import javafx.application.Platform;
import javafx.scene.Cursor;
import javafx.scene.Node;
public class ModelMassEditAction {
static BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue);
protected List<? extends Model> models;
protected Consumer<Model> action;
protected Node source;
protected ModelMassEditAction(Node source, List<? extends Model> models) {
this.source = source;
this.models = models;
}
public ModelMassEditAction(Node source, List<? extends Model> models, Consumer<Model> action) {
this.source = source;
this.models = models;
this.action = action;
}
public void execute() {
execute((m) -> {});
}
public void execute(Consumer<Model> callback) {
Consumer<Model> cb = Objects.requireNonNull(callback);
source.setCursor(Cursor.WAIT);
threadPool.submit(() -> {
for (Model model : models) {
action.accept(model);
cb.accept(model);
}
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));
});
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.action;
import java.util.List;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Node;
public class PauseAction extends ModelMassEditAction {
public PauseAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = (m) -> {
try {
recorder.suspendRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError("Couldn't suspend recording of model", "Suspending recording of " + m.getName() + " failed", e));
}
};
}
}

View File

@ -0,0 +1,33 @@
package ctbrec.ui.action;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ui.Player;
import ctbrec.ui.controls.Toast;
import javafx.application.Platform;
import javafx.scene.Cursor;
import javafx.scene.Node;
public class PlayAction {
private Model selectedModel;
private Node source;
public PlayAction(Node source, Model selectedModel) {
this.source = source;
this.selectedModel = selectedModel;
}
public void execute() {
source.setCursor(Cursor.WAIT);
new Thread(() -> {
boolean started = Player.play(selectedModel);
Platform.runLater(() -> {
if (started && Config.getInstance().getSettings().showPlayerStarting) {
Toast.makeText(source.getScene(), "Starting Player", 2000, 500, 500);
}
source.setCursor(Cursor.DEFAULT);
});
}).start();
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.action;
import java.util.List;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Node;
public class ResumeAction extends ModelMassEditAction {
public ResumeAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = (m) -> {
try {
recorder.resumeRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError("Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed", e));
}
};
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.action;
import java.util.List;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Node;
public class StartRecordingAction extends ModelMassEditAction {
public StartRecordingAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = (m) -> {
try {
recorder.startRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError("Couldn't start recording", "Starting recording of " + m.getName() + " failed", e));
}
};
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.action;
import java.util.List;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Node;
public class StopRecordingAction extends ModelMassEditAction {
public StopRecordingAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models);
action = (m) -> {
try {
recorder.stopRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
Dialogs.showError("Couldn't stop recording", "Stopping recording of " + m.getName() + " failed", e));
}
};
}
}

View File

@ -0,0 +1,146 @@
package ctbrec.ui.controls;
import java.io.File;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.StringUtil;
import ctbrec.ui.AutosizeAlert;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
public abstract class AbstractFileSelectionBox extends HBox {
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractFileSelectionBox.class);
// private ObjectProperty<File> fileProperty = new ObjectPropertyBase<File>() {
// @Override
// public Object getBean() {
// return null;
// }
//
// @Override
// public String getName() {
// return "file";
// }
// };
private StringProperty fileProperty = new SimpleStringProperty();
protected TextField fileInput;
protected boolean allowEmptyValue = false;
private Tooltip validationError = new Tooltip();
public AbstractFileSelectionBox() {
super(5);
fileInput = new TextField();
fileInput.textProperty().addListener(textListener());
fileInput.focusedProperty().addListener((obs, o, n) -> {
if(!n) {
validationError.hide();
}
});
Node browse = createBrowseButton();
getChildren().addAll(fileInput, browse);
fileInput.disableProperty().bind(disableProperty());
browse.disableProperty().bind(disableProperty());
HBox.setHgrow(fileInput, Priority.ALWAYS);
}
public AbstractFileSelectionBox(String initialValue) {
this();
fileInput.setText(initialValue);
}
private ChangeListener<? super String> textListener() {
return (obs, o, n) -> {
String input = fileInput.getText();
if(StringUtil.isBlank(input) && allowEmptyValue) {
fileProperty.set("");
hideValidationHints();
return;
} else {
File program = new File(input);
setFile(program);
}
};
}
protected void setFile(File file) {
String msg = validate(file);
if (msg != null) {
fileInput.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2))));
validationError.setText(msg);
fileInput.setTooltip(validationError);
Point2D p = fileInput.localToScreen(fileInput.getTranslateY(), fileInput.getTranslateY());
if(!validationError.isShowing() && getScene() != null) {
validationError.show(getScene().getWindow(), p.getX(), p.getY() + fileInput.getHeight() + 4);
}
} else {
fileProperty.set(file.getAbsolutePath());
hideValidationHints();
}
}
private void hideValidationHints() {
fileInput.setBorder(Border.EMPTY);
fileInput.setTooltip(null);
validationError.hide();
}
protected String validate(File file) {
if (file == null || !file.exists()) {
return "File does not exist";
} else {
return null;
}
}
public void allowEmptyValue() {
this.allowEmptyValue = true;
}
private Button createBrowseButton() {
Button button = new Button("Select");
button.setOnAction((e) -> {
choose();
});
return button;
}
protected void choose() {
FileChooser chooser = new FileChooser();
File program = chooser.showOpenDialog(null);
if(program != null) {
try {
fileInput.setText(program.getCanonicalPath());
} catch (IOException e1) {
LOG.error("Couldn't determine path", e1);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't determine path");
alert.showAndWait();
}
setFile(program);
}
}
public StringProperty fileProperty() {
return fileProperty;
}
}

View File

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

View File

@ -0,0 +1,27 @@
package ctbrec.ui.controls;
import ctbrec.ui.AutosizeAlert;
import javafx.application.Platform;
import javafx.scene.control.Alert;
public class Dialogs {
public static void showError(String header, String text, Throwable t) {
Runnable r = () -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(header);
String content = text;
if(t != null) {
content += " " + t.getLocalizedMessage();
}
alert.setContentText(content);
alert.showAndWait();
};
if(Platform.isFxApplicationThread()) {
r.run();
} else {
Platform.runLater(r);
}
}
}

View File

@ -0,0 +1,37 @@
package ctbrec.ui.controls;
import java.io.File;
import javafx.stage.DirectoryChooser;
public class DirectorySelectionBox extends AbstractFileSelectionBox {
public DirectorySelectionBox(String dir) {
super(dir);
}
@Override
protected void choose() {
DirectoryChooser chooser = new DirectoryChooser();
File currentDir = new File(fileProperty().get());
if (currentDir.exists() && currentDir.isDirectory()) {
chooser.setInitialDirectory(currentDir);
}
File selectedDir = chooser.showDialog(null);
if(selectedDir != null) {
fileInput.setText(selectedDir.getAbsolutePath());
setFile(selectedDir);
}
}
@Override
protected String validate(File file) {
String msg = super.validate(file);
if(msg != null) {
return msg;
} else if (!file.isDirectory()) {
return "This is not a directory";
} else {
return null;
}
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.controls;
import java.io.File;
public class FileSelectionBox extends AbstractFileSelectionBox {
public FileSelectionBox() {
}
public FileSelectionBox(String initialValue) {
super(initialValue);
}
@Override
protected String validate(File file) {
String msg = super.validate(file);
if(msg != null) {
return msg;
} else if (!file.isFile()) {
return "This is not a regular file";
} else {
return null;
}
}
}

View File

@ -0,0 +1,28 @@
.popover {
-fx-padding: 43 7 7 7;
}
.popover-frame {
-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
-fx-background-radius: 10px, 10px, 10px, 10px;
-fx-padding: 1;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);
}
.popover.left-tooth .popover-frame {
-fx-shape: "m 33.34215,51.52967 4.782653,4.746482 4.333068,4.299995 h 94.637639 c 1.108,0 1.99987,0.891879 1.99987,1.999877 V 164.22046 c 0,1.10801 -0.89187,1.99988 -1.99987,1.99988 H 12.205971 c -1.107998,0 -2.000392,-0.89187 -2.000392,-1.99988 V 62.576024 c 0,-1.107998 0.892394,-1.999877 2.000392,-1.999877 h 12.020455 l 4.333071,-4.299995 z";
}
.popover.right-tooth .popover-frame {
-fx-shape: "M 438.26953 194.75781 L 420.19336 212.69727 L 403.81641 228.94922 L 46.130859 228.94922 C 41.943143 228.94922 38.572266 232.3201 38.572266 236.50781 L 38.572266 620.67578 C 38.572266 624.8635 41.943143 628.23438 46.130859 628.23438 L 518.1543 628.23438 C 522.34201 628.23438 525.71484 624.8635 525.71484 620.67578 L 525.71484 236.50781 C 525.71484 232.3201 522.34201 228.94922 518.1543 228.94922 L 472.72266 228.94922 L 456.3457 212.69727 L 438.26953 194.75781 z";
}
.popover-title {
-fx-font-size: 20px;
-fx-text-fill: -fx-text-background-color;
}
.popover .button {
-fx-font-size: 12px;
}

View File

@ -0,0 +1,469 @@
/*
* 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.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.TextAlignment;
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 Label title; // the current title
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() {
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);
pagesClipRect.setArcHeight(10);
pagesClipRect.arcWidthProperty().bind(pagesClipRect.arcHeightProperty());
pagesPane.setClip(pagesClipRect);
getChildren().addAll(frameBorder, titlesPane, leftButton, rightButton, pagesPane);
// 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() + 40;
final int right = (int)insets.getRight();
final int bottom = (int)insets.getBottom();
final int left = (int)insets.getLeft();
final int offset = 18;
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 + offset,leftButtonWidth,buttonHeight);
final int rightButtonWidth = (int)snapSizeX(rightButton.prefWidth(-1));
rightButton.resizeRelocate(width-right-rightButtonWidth, buttonTop + offset,rightButtonWidth,buttonHeight);
if (title != null) {
double tw = title.getWidth();
double th = title.getHeight();
title.setTranslateX((width - tw) / 2);
title.setTranslateY((top - th) / 2 + offset);
}
}
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);
}
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)
)
).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 Label(page.getPageTitle());
title.getStyleClass().add("popover-title");
title.setTextAlignment(TextAlignment.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)
)
);
timeline.play();
}
page.setPopover(this);
page.handleShown();
pages.push(page);
}
private void resizePopoverToNewPage(final Node newPageNode) {
final Insets insets = getInsets();
final double width = prefWidth(-1);
final double contentWidth = width - insets.getLeft() - insets.getRight();
double h = newPageNode.prefHeight(contentWidth);
h += insets.getTop() + insets.getBottom();
new Timeline(
new KeyFrame(Duration.millis(200),
new KeyValue(popoverHeight, h, Interpolator.EASE_BOTH)
)
).play();
}
public void show(){
show(null);
}
private Animation fadeAnimation = null;
public void show(Runnable onHideCallback){
if (!isVisible() || fadeAnimation != null) {
this.onHideCallback = onHideCallback;
getScene().addEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler);
// getScene().addEventFilter(ScrollEvent.ANY,popoverScrollHandler);
if (fadeAnimation != null) {
fadeAnimation.stop();
setVisible(true); // for good measure
} else {
popoverHeight.set(-1);
setVisible(true);
}
FadeTransition fade = new FadeTransition(Duration.seconds(.1), this);
fade.setToValue(1.0);
fade.setOnFinished((ActionEvent event) -> {
fadeAnimation = null;
});
ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this);
scale.setToX(1);
scale.setToY(1);
ParallelTransition tx = new ParallelTransition(fade, scale);
fadeAnimation = tx;
tx.play();
}
}
public void hide(){
if (isVisible() || fadeAnimation != null) {
getScene().removeEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler);
// getScene().removeEventFilter(ScrollEvent.ANY,popoverScrollHandler);
if (fadeAnimation != null) {
fadeAnimation.stop();
}
FadeTransition fade = new FadeTransition(Duration.seconds(.1), this);
fade.setToValue(0);
fade.setOnFinished((ActionEvent event) -> {
fadeAnimation = null;
setVisible(false);
//clearPages();
if (onHideCallback != null) onHideCallback.run();
});
ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this);
scale.setToX(.8);
scale.setToY(.8);
ParallelTransition tx = new ParallelTransition(fade, scale);
fadeAnimation = tx;
tx.play();
}
}
/**
* Represents a page in a popover.
*/
public static interface Page {
public void setPopover(Popover popover);
public Popover getPopover();
/**
* Get the node that represents the page.
*
* @return the page node.
*/
public Node getPageNode();
/**
* Get the title to display for this page.
*
* @return The page title
*/
public String getPageTitle();
/**
* The text for left button, if null then button will be hidden.
* @return The button text
*/
public String leftButtonText();
/**
* Called on a click of the left button of the popover.
*/
public void handleLeftButton();
/**
* The text for right button, if null then button will be hidden.
* @return The button text
*/
public String rightButtonText();
/**
* Called on a click of the right button of the popover.
*/
public void handleRightButton();
public void handleShown();
public void handleHidden();
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.util.Callback;
/**
* Special ListView designed to look like "Text... &gt;" tree list. Perhaps we ought to have customized
* a TreeView instead of a ListView (as the TreeView already has the data model all defined).
*
* This implementation minimizes classes by just having the PopoverTreeList implementing everything
* (it is the Control, the Skin, and the CellFactory all in one).
*/
public class PopoverTreeList<T> extends ListView<T> implements Callback<ListView<T>, ListCell<T>> {
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 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;
}
// 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);
} else {
setText(item.toString());
}
}
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.ui.controls;
import java.io.File;
public class ProgramSelectionBox extends FileSelectionBox {
public ProgramSelectionBox() {
}
public ProgramSelectionBox(String initialValue) {
super(initialValue);
}
@Override
protected String validate(File file) {
String msg = super.validate(file);
if(msg != null) {
return msg;
} else if (!file.canExecute()) {
return "This is not an executable application";
} else {
return null;
}
}
}

View File

@ -0,0 +1,34 @@
.search-box-icon {
-fx-shape: "M10.728,9.893c0.889-1.081,1.375-2.435,1.375-3.842C12.103,2.714,9.388,0,6.051,0C2.715,0,0,2.714,0,6.051c0,3.338,2.715,6.052,6.051,6.052c0.954,0,1.898-0.227,2.744-0.656l3.479,3.478l1.743-1.742L10.728,9.893z M6.051,2.484c1.966,0,3.566,1.602,3.566,3.566c0,1.968-1.6,3.567-3.566,3.567c-1.967,0-3.566-1.6-3.566-3.567C2.485,4.086,4.084,2.484,6.051,2.484z";
-fx-scale-shape: false;
-fx-background-color: -fx-mark-color;
}
.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: -fx-mark-color;
-fx-padding: 9.5px;
}
.search-tree-list-cell {
-fx-background-color: -fx-background;
-fx-border-color: transparent transparent -fx-base transparent;
-fx-padding: 0 30 0 20;
-fx-font-size: 15px;
-fx-text-fill: -fx-mid-text-color;
-fx-graphic-text-gap: 20px;
}
.highlight {
-fx-background-color: -fx-focus-color;
-fx-text-fill: -fx-light-text-color;
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2008, 2014, Oracle and/or its affiliates.
* All rights reserved. Use is subject to license terms.
*
* This file is available and licensed under the following license:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the distribution.
* - Neither the name of Oracle Corporation nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package ctbrec.ui.controls;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
/**
* Search field with styling and a clear button
*/
public class SearchBox extends TextField implements ChangeListener<String>{
private final Button clearButton = new Button();
private final Region innerBackground = new Region();
private final Region icon = new Region();
private final int prefHeight = 26;
public SearchBox() {
getStyleClass().addAll("search-box");
icon.getStyleClass().setAll("search-box-icon");
innerBackground.getStyleClass().setAll("search-box-inner");
setPromptText("Search");
textProperty().addListener(this);
setPrefHeight(prefHeight);
clearButton.getStyleClass().setAll("search-clear-button");
clearButton.setCursor(Cursor.DEFAULT);
clearButton.setOnMouseClicked((MouseEvent t) -> {
setText("");
requestFocus();
});
clearButton.setVisible(false);
clearButton.setManaged(false);
innerBackground.setManaged(false);
icon.setManaged(false);
}
public SearchBox(boolean icon) {
this();
this.icon.setVisible(false);
this.icon.getStyleClass().remove("search-box-icon");
this.setStyle("-fx-padding: 5");
}
@Override protected void layoutChildren() {
super.layoutChildren();
if (clearButton.getParent() != this) getChildren().add(clearButton);
if (innerBackground.getParent() != this) getChildren().add(0,innerBackground);
if (icon.getParent() != this) getChildren().add(icon);
innerBackground.setLayoutX(0);
innerBackground.setLayoutY(0);
innerBackground.resize(getWidth(), getHeight());
icon.setLayoutX(0);
icon.setLayoutY(0);
icon.resize(35,prefHeight);
clearButton.setLayoutX(getWidth() - prefHeight);
clearButton.setLayoutY(0);
clearButton.resize(prefHeight, prefHeight);
}
@Override public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) {
clearButton.setVisible(newValue.length() > 0);
}
}

View File

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

View File

@ -0,0 +1,323 @@
/*
* 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.net.URL;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.action.PlayAction;
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;
import javafx.scene.shape.Rectangle;
/**
* 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;
}
new PlayAction(this, model).execute();
}
@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");
});
Rectangle clip = new Rectangle(thumbSize, thumbSize);
clip.setArcWidth(20);
clip.arcHeightProperty().bind(clip.arcWidthProperty());
thumb.setFitWidth(thumbSize);
thumb.setFitHeight(thumbSize);
thumb.setClip(clip);
thumb.setSmooth(true);
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.getDisplayName());
this.model = model;
URL anonymousPng = getClass().getResource("/anonymous.png");
String previewUrl = Optional.ofNullable(model.getPreview()).orElse(anonymousPng.toString());
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
Image img = new Image(previewUrl, true);
thumb.setImage(img);
} else {
Image img = new Image(anonymousPng.toString(), 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;
}
}

View File

@ -0,0 +1,189 @@
package ctbrec.ui.controls;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
public class StreamPreview extends StackPane {
private static final transient Logger LOG = LoggerFactory.getLogger(StreamPreview.class);
private ImageView preview = new ImageView();
private MediaView videoPreview;
private MediaPlayer videoPlayer;
private Media video;
private ProgressIndicator progressIndicator;
private static ExecutorService executor = Executors.newSingleThreadExecutor();
private static Future<?> future;
public StreamPreview() {
videoPreview = new MediaView();
videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16);
videoPreview.setPreserveRatio(true);
StackPane.setMargin(videoPreview, new Insets(5));
preview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
preview.setPreserveRatio(true);
preview.setSmooth(true);
preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;");
preview.visibleProperty().bind(videoPreview.visibleProperty().not());
StackPane.setMargin(preview, new Insets(5));
progressIndicator = new ProgressIndicator();
progressIndicator.setVisible(false);
Region veil = new Region();
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)");
veil.visibleProperty().bind(progressIndicator.visibleProperty());
StackPane.setMargin(veil, new Insets(5));
getChildren().addAll(preview, videoPreview, veil, progressIndicator);
}
public void startStream(Model model) {
Platform.runLater(() -> {
progressIndicator.setVisible(true);
if(model.getPreview() != null) {
try {
videoPreview.setVisible(false);
Image img = new Image(model.getPreview(), true);
preview.setImage(img);
double aspect = img.getWidth() / img.getHeight();
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resizeTo(w, h);
} catch (Exception e) {}
}
});
if(future != null && !future.isDone()) {
future.cancel(true);
}
future = executor.submit(() -> {
try {
List<StreamSource> sources = model.getStreamSources();
Collections.sort(sources);
StreamSource best = sources.get(0);
checkInterrupt();
LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl());
video = new Media(best.getMediaPlaylistUrl());
video.setOnError(() -> onError(videoPlayer));
if(videoPlayer != null) {
videoPlayer.dispose();
}
videoPlayer = new MediaPlayer(video);
videoPlayer.setMute(true);
checkInterrupt();
videoPlayer.setOnReady(() -> {
if(!future.isCancelled()) {
Platform.runLater(() -> {
double aspect = (double)video.getWidth() / video.getHeight();
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resizeTo(w, h);
progressIndicator.setVisible(false);
videoPreview.setVisible(true);
videoPreview.setMediaPlayer(videoPlayer);
videoPlayer.play();
});
}
});
videoPlayer.setOnError(() -> onError(videoPlayer));
} catch (IllegalStateException e) {
if(e.getMessage().equals("Stream url unknown")) {
// fine hls url for mfc not known yet
} else {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
}
showTestImage();
} catch (HttpException e) {
if(e.getResponseCode() != 404) {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
}
showTestImage();
} catch (InterruptedException | InterruptedIOException e) {
// future has been canceled, that's fine
} catch (ExecutionException e) {
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
// future has been canceled, that's fine
} else {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
}
showTestImage();
} catch (Exception e) {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
showTestImage();
}
});
}
public void resizeTo(double w, double h) {
preview.setFitWidth(w);
preview.setFitHeight(h);
videoPreview.setFitWidth(w);
videoPreview.setFitHeight(h);
progressIndicator.setPrefSize(w, h);
}
public void stop() {
MediaPlayer old = videoPlayer;
Future<?> oldFuture = future;
new Thread(() -> {
if(oldFuture != null && !oldFuture.isDone()) {
oldFuture.cancel(true);
}
if(old != null) {
old.dispose();
}
}).start();
}
private void onError(MediaPlayer videoPlayer) {
LOG.error("Error while starting preview stream", videoPlayer.getError());
if(videoPlayer.getError().getCause() != null) {
LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause());
}
showTestImage();
}
private void showTestImage() {
stop();
Platform.runLater(() -> {
videoPreview.setVisible(false);
Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true);
preview.setImage(img);
double aspect = img.getWidth() / img.getHeight();
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resizeTo(w, h);
progressIndicator.setVisible(false);
});
}
private void checkInterrupt() throws InterruptedException {
if(Thread.interrupted()) {
throw new InterruptedException();
}
}
}

View File

@ -0,0 +1,53 @@
package ctbrec.ui.controls;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
public final class Toast {
public static void makeText(Scene owner, String toastMsg, int toastDelay, int fadeInDelay, int fadeOutDelay) {
Stage toastStage = new Stage();
toastStage.initOwner(owner.getWindow());
toastStage.setResizable(false);
toastStage.initStyle(StageStyle.TRANSPARENT);
Text text = new Text(toastMsg);
text.setFont(Font.font(30));
text.setFill(Color.WHITE);
StackPane root = new StackPane(text);
root.setStyle("-fx-background-radius: 20; -fx-background-color: rgba(0, 0, 0, 0.8); -fx-padding: 50px;");
root.setOpacity(0);
Scene scene = new Scene(root);
scene.setFill(Color.TRANSPARENT);
toastStage.setScene(scene);
toastStage.show();
Timeline fadeInTimeline = new Timeline();
KeyFrame fadeInKey1 = new KeyFrame(Duration.millis(fadeInDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 1));
fadeInTimeline.getKeyFrames().add(fadeInKey1);
fadeInTimeline.setOnFinished((ae) -> {
new Thread(() -> {
try {
Thread.sleep(toastDelay);
} catch (InterruptedException e) {
}
Timeline fadeOutTimeline = new Timeline();
KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0));
fadeOutTimeline.getKeyFrames().add(fadeOutKey1);
fadeOutTimeline.setOnFinished((aeb) -> toastStage.close());
fadeOutTimeline.play();
}).start();
});
fadeInTimeline.play();
}
}

View File

@ -0,0 +1,102 @@
package ctbrec.ui.controls;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Wizard extends BorderPane {
private static final transient Logger LOG = LoggerFactory.getLogger(Wizard.class);
private Pane[] pages;
private StackPane stack;
private Stage stage;
private int page = 0;
private Button next;
private Button prev;
private Button finish;
private boolean cancelled = true;
private Runnable validator;
public Wizard(Stage stage, Runnable validator, Pane... pages) {
this.stage = stage;
this.validator = validator;
this.pages = pages;
if (pages.length == 0) {
throw new IllegalArgumentException("Provide at least one page");
}
createUi();
}
private void createUi() {
stack = new StackPane();
setCenter(stack);
next = new Button("Next");
next.setOnAction(evt -> nextPage());
prev = new Button("Back");
prev.setOnAction(evt -> prevPage());
prev.visibleProperty().bind(next.visibleProperty());
next.setVisible(pages.length > 1);
Button cancel = new Button("Cancel");
cancel.setOnAction(evt -> stage.close());
finish = new Button("Finish");
finish.setOnAction(evt -> {
if(validator != null) {
try {
validator.run();
} catch(IllegalStateException e) {
Dialogs.showError("Settings invalid", e.getMessage(), null);
return;
}
}
cancelled = false;
stage.close();
});
HBox buttons = new HBox(5, prev, next, cancel, finish);
buttons.setAlignment(Pos.BASELINE_RIGHT);
setBottom(buttons);
BorderPane.setMargin(buttons, new Insets(10));
if (pages.length != 0) {
prevPage();
}
}
private void prevPage() {
page = Math.max(0, --page);
stack.getChildren().clear();
stack.getChildren().add(pages[page]);
updateState();
}
private void nextPage() {
page = Math.min(pages.length - 1, ++page);
stack.getChildren().clear();
stack.getChildren().add(pages[page]);
updateState();
}
private void updateState() {
prev.setDisable(page == 0);
next.setDisable(page == pages.length - 1);
finish.setDisable(page != pages.length - 1);
LOG.debug("Setting border");
pages[page].setStyle(
"-fx-background-color: -fx-inner-border, -fx-background;"+
"-fx-background-insets: 0 0 -1 0, 0, 1, 2;");
}
public boolean isCancelled() {
return cancelled;
}
}

View File

@ -0,0 +1,35 @@
package ctbrec.ui.event;
import java.io.File;
import java.net.URL;
import ctbrec.event.Action;
import ctbrec.event.Event;
import ctbrec.event.EventHandlerConfiguration.ActionConfiguration;
import javafx.scene.media.AudioClip;
public class PlaySound extends Action {
private URL url;
public PlaySound() {
name = "play sound";
}
public PlaySound(URL url) {
this();
this.url = url;
}
@Override
public void accept(Event evt) {
AudioClip clip = new AudioClip(url.toString());
clip.play();
}
@Override
public void configure(ActionConfiguration config) throws Exception {
File file = new File((String) config.getConfiguration().get("file"));
url = file.toURI().toURL();
}
}

View File

@ -0,0 +1,42 @@
package ctbrec.ui.event;
import ctbrec.Model;
import ctbrec.OS;
import ctbrec.event.Action;
import ctbrec.event.Event;
import ctbrec.event.EventHandlerConfiguration.ActionConfiguration;
import ctbrec.event.ModelStateChangedEvent;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.ui.CamrecApplication;
public class ShowNotification extends Action {
public ShowNotification() {
name = "show notification";
}
@Override
public void accept(Event evt) {
String header = evt.getType().toString();
String msg;
switch(evt.getType()) {
case MODEL_STATUS_CHANGED:
ModelStateChangedEvent modelEvent = (ModelStateChangedEvent) evt;
Model m = modelEvent.getModel();
msg = m.getDisplayName() + " is now " + modelEvent.getNewState().toString();
break;
case RECORDING_STATUS_CHANGED:
RecordingStateChangedEvent recEvent = (RecordingStateChangedEvent) evt;
m = recEvent.getModel();
msg = "Recording for model " + m.getDisplayName() + " is now in state " + recEvent.getState().toString();
break;
default:
msg = evt.getDescription();
}
OS.notification(CamrecApplication.title, header, msg);
}
@Override
public void configure(ActionConfiguration config) throws Exception {
}
}

View File

@ -0,0 +1,319 @@
package ctbrec.ui.settings;
import java.io.File;
import java.io.InputStream;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.event.Event;
import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.event.EventHandlerConfiguration.ActionConfiguration;
import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration;
import ctbrec.event.ExecuteProgram;
import ctbrec.event.ModelPredicate;
import ctbrec.event.ModelStatePredicate;
import ctbrec.event.RecordingStatePredicate;
import ctbrec.recorder.Recorder;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.controls.FileSelectionBox;
import ctbrec.ui.controls.ProgramSelectionBox;
import ctbrec.ui.controls.Wizard;
import ctbrec.ui.event.PlaySound;
import ctbrec.ui.event.ShowNotification;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Separator;
import javafx.scene.control.TextField;
import javafx.scene.control.TitledPane;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
public class ActionSettingsPanel extends TitledPane {
private static final transient Logger LOG = LoggerFactory.getLogger(ActionSettingsPanel.class);
private ListView<EventHandlerConfiguration> actionTable;
private TextField name = new TextField();
private ComboBox<Event.Type> event = new ComboBox<>();
private ComboBox<Model.State> modelState = new ComboBox<>();
private ComboBox<Recording.State> recordingState = new ComboBox<>();
private CheckBox playSound = new CheckBox("Play sound");
private FileSelectionBox sound = new FileSelectionBox();
private CheckBox showNotification = new CheckBox("Notify me");
private Button testNotification = new Button("Test");
private CheckBox executeProgram = new CheckBox("Execute program");
private ProgramSelectionBox program = new ProgramSelectionBox();
private ListSelectionPane<Model> modelSelectionPane;
private Recorder recorder;
public ActionSettingsPanel(SettingsTab settingsTab, Recorder recorder) {
this.recorder = recorder;
setText("Events & Actions");
setExpanded(true);
setCollapsible(false);
createGui();
loadEventHandlers();
}
private void loadEventHandlers() {
actionTable.getItems().addAll(Config.getInstance().getSettings().eventHandlers);
}
private void createGui() {
BorderPane mainLayout = new BorderPane();
setContent(mainLayout);
actionTable = createActionTable();
ScrollPane scrollPane = new ScrollPane(actionTable);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background-color: -fx-background");
mainLayout.setCenter(scrollPane);
Button add = new Button("Add");
add.setOnAction(this::add);
Button delete = new Button("Delete");
delete.setOnAction(this::delete);
delete.setDisable(true);
HBox buttons = new HBox(5, add, delete);
mainLayout.setBottom(buttons);
BorderPane.setMargin(buttons, new Insets(5, 0, 0, 0));
actionTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<EventHandlerConfiguration>() {
@Override
public void onChanged(Change<? extends EventHandlerConfiguration> change) {
delete.setDisable(change.getList().isEmpty());
}
});
}
private void add(ActionEvent evt) {
Pane actionPane = createActionPane();
Stage dialog = new Stage();
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.initOwner(getScene().getWindow());
dialog.setTitle("New Action");
InputStream icon = getClass().getResourceAsStream("/icon.png");
dialog.getIcons().add(new Image(icon));
Wizard root = new Wizard(dialog, this::validateSettings, actionPane);
Scene scene = new Scene(root, 800, 540);
scene.getStylesheets().addAll(getScene().getStylesheets());
dialog.setScene(scene);
centerOnParent(dialog);
dialog.showAndWait();
if(!root.isCancelled()) {
createEventHandler();
}
}
private void createEventHandler() {
EventHandlerConfiguration config = new EventHandlerConfiguration();
config.setName(name.getText());
config.setEvent(event.getValue());
if(event.getValue() == Event.Type.MODEL_STATUS_CHANGED) {
PredicateConfiguration pc = new PredicateConfiguration();
pc.setType(ModelStatePredicate.class.getName());
pc.getConfiguration().put("state", modelState.getValue().name());
pc.setName("state = " + modelState.getValue().toString());
config.getPredicates().add(pc);
} else if(event.getValue() == Event.Type.RECORDING_STATUS_CHANGED) {
PredicateConfiguration pc = new PredicateConfiguration();
pc.setType(RecordingStatePredicate.class.getName());
pc.getConfiguration().put("state", recordingState.getValue().name());
pc.setName("state = " + recordingState.getValue().toString());
config.getPredicates().add(pc);
}
if(!modelSelectionPane.isAllSelected()) {
PredicateConfiguration pc = new PredicateConfiguration();
pc.setType(ModelPredicate.class.getName());
pc.setModels(modelSelectionPane.getSelectedItems());
pc.setName("model is one of:" + modelSelectionPane.getSelectedItems());
config.getPredicates().add(pc);
}
if(showNotification.isSelected()) {
ActionConfiguration ac = new ActionConfiguration();
ac.setType(ShowNotification.class.getName());
ac.setName("show notification");
config.getActions().add(ac);
}
if(playSound.isSelected()) {
ActionConfiguration ac = new ActionConfiguration();
ac.setType(PlaySound.class.getName());
File file = new File(sound.fileProperty().get());
ac.getConfiguration().put("file", file.getAbsolutePath());
ac.setName("play " + file.getName());
config.getActions().add(ac);
}
if(executeProgram.isSelected()) {
ActionConfiguration ac = new ActionConfiguration();
ac.setType(ExecuteProgram.class.getName());
File file = new File(program.fileProperty().get());
ac.getConfiguration().put("file", file.getAbsolutePath());
ac.setName("execute " + file.getName());
config.getActions().add(ac);
}
EventHandler handler = new EventHandler(config);
EventBusHolder.register(handler);
Config.getInstance().getSettings().eventHandlers.add(config);
actionTable.getItems().add(config);
LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName());
}
private void validateSettings() {
if(StringUtil.isBlank(name.getText())) {
throw new IllegalStateException("Name cannot be empty");
}
if(event.getValue() == Event.Type.MODEL_STATUS_CHANGED && modelState.getValue() == null) {
throw new IllegalStateException("Select a state");
}
if(event.getValue() == Event.Type.RECORDING_STATUS_CHANGED && recordingState.getValue() == null) {
throw new IllegalStateException("Select a state");
}
if(modelSelectionPane.getSelectedItems().isEmpty() && !modelSelectionPane.isAllSelected()) {
throw new IllegalStateException("Select one or more models or tick off \"all\"");
}
if(!(showNotification.isSelected() || playSound.isSelected() || executeProgram.isSelected())) {
throw new IllegalStateException("No action selected");
}
}
private void delete(ActionEvent evt) {
List<EventHandlerConfiguration> selected = new ArrayList<>(actionTable.getSelectionModel().getSelectedItems());
for (EventHandlerConfiguration config : selected) {
EventBusHolder.unregister(config.getId());
Config.getInstance().getSettings().eventHandlers.remove(config);
actionTable.getItems().remove(config);
}
}
private Pane createActionPane() {
GridPane layout = SettingsTab.createGridLayout();
recordingState.prefWidthProperty().bind(event.widthProperty());
modelState.prefWidthProperty().bind(event.widthProperty());
name.prefWidthProperty().bind(event.widthProperty());
int row = 0;
layout.add(new Label("Name"), 0, row);
layout.add(name, 1, row++);
layout.add(new Label("Event"), 0, row);
event.getItems().add(Event.Type.MODEL_STATUS_CHANGED);
event.getItems().add(Event.Type.RECORDING_STATUS_CHANGED);
event.setOnAction(evt -> {
modelState.setVisible(event.getSelectionModel().getSelectedItem() == Event.Type.MODEL_STATUS_CHANGED);
});
event.getSelectionModel().select(Event.Type.MODEL_STATUS_CHANGED);
layout.add(event, 1, row++);
layout.add(new Label("State"), 0, row);
modelState.getItems().clear();
modelState.getItems().addAll(Model.State.values());
layout.add(modelState, 1, row);
recordingState.getItems().clear();
recordingState.getItems().addAll(Recording.State.values());
layout.add(recordingState, 1, row++);
recordingState.visibleProperty().bind(modelState.visibleProperty().not());
layout.add(createSeparator(), 0, row++);
Label l = new Label("Models");
layout.add(l, 0, row);
modelSelectionPane = new ListSelectionPane<Model>(recorder.getModelsRecording(), Collections.emptyList());
layout.add(modelSelectionPane, 1, row++);
GridPane.setValignment(l, VPos.TOP);
GridPane.setHgrow(modelSelectionPane, Priority.ALWAYS);
GridPane.setFillWidth(modelSelectionPane, true);
layout.add(createSeparator(), 0, row++);
layout.add(showNotification, 0, row);
layout.add(testNotification, 1, row++);
testNotification.setOnAction(evt -> {
DateTimeFormatter format = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
ZonedDateTime time = ZonedDateTime.now();
OS.notification(CamrecApplication.title, "Test Notification", "Oi, what's up! " + format.format(time));
});
testNotification.disableProperty().bind(showNotification.selectedProperty().not());
layout.add(playSound, 0, row);
layout.add(sound, 1, row++);
sound.disableProperty().bind(playSound.selectedProperty().not());
layout.add(executeProgram, 0, row);
layout.add(program, 1, row);
program.disableProperty().bind(executeProgram.selectedProperty().not());
GridPane.setFillWidth(name, true);
GridPane.setHgrow(name, Priority.ALWAYS);
GridPane.setFillWidth(sound, true);
return layout;
}
private ListView<EventHandlerConfiguration> createActionTable() {
ListView<EventHandlerConfiguration> view = new ListView<>();
view.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
view.setPrefSize(300, 200);
return view;
}
private Node createSeparator() {
Separator divider = new Separator(Orientation.HORIZONTAL);
GridPane.setHgrow(divider, Priority.ALWAYS);
GridPane.setFillWidth(divider, true);
GridPane.setColumnSpan(divider, 2);
int tb = 20;
int lr = 0;
GridPane.setMargin(divider, new Insets(tb, lr, tb, lr));
return divider;
}
private void centerOnParent(Stage dialog) {
dialog.setWidth(dialog.getScene().getWidth());
dialog.setHeight(dialog.getScene().getHeight());
double w = dialog.getWidth();
double h = dialog.getHeight();
Window p = dialog.getOwner();
double px = p.getX();
double py = p.getY();
double pw = p.getWidth();
double ph = p.getHeight();
dialog.setX(px + (pw - w) / 2);
dialog.setY(py + (ph - h) / 2);
}
}

View File

@ -0,0 +1,10 @@
ColorSettingsPane .color-picker .color-picker-label .text {
visibility: false;
}
/*
ColorSettingsPane .color-picker > .arrow-button,
ColorSettingsPane .color-picker > .arrow-button:hover
{
visibility: false;
}
*/

View File

@ -0,0 +1,77 @@
package ctbrec.ui.settings;
import ctbrec.Config;
import javafx.scene.control.Button;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
public class ColorSettingsPane extends Pane {
ColorPicker baseColor = new ColorPicker();
ColorPicker accentColor = new ColorPicker();
Button reset = new Button("Reset");
Pane foobar = new Pane();
public ColorSettingsPane(SettingsTab settingsTab) {
getChildren().add(baseColor);
getChildren().add(accentColor);
getChildren().add(reset);
baseColor.setValue(Color.web(Config.getInstance().getSettings().colorBase));
baseColor.setTooltip(new Tooltip("Base Color"));
accentColor.setValue(Color.web(Config.getInstance().getSettings().colorAccent));
accentColor.setTooltip(new Tooltip("Accent Color"));
baseColor.setOnAction(evt -> {
Config.getInstance().getSettings().colorBase = toWeb(baseColor.getValue());
settingsTab.showRestartRequired();
settingsTab.saveConfig();
});
accentColor.setOnAction(evt -> {
Config.getInstance().getSettings().colorAccent = toWeb(accentColor.getValue());
settingsTab.showRestartRequired();
settingsTab.saveConfig();
});
reset.setOnAction(evt -> {
baseColor.setValue(Color.WHITE);
Config.getInstance().getSettings().colorBase = toWeb(Color.WHITE);
accentColor.setValue(Color.WHITE);
Config.getInstance().getSettings().colorAccent = toWeb(Color.WHITE);
settingsTab.showRestartRequired();
settingsTab.saveConfig();
});
}
private String toWeb(Color value) {
StringBuilder sb = new StringBuilder("#");
sb.append(toHex((int) (value.getRed() * 255)));
sb.append(toHex((int) (value.getGreen() * 255)));
sb.append(toHex((int) (value.getBlue() * 255)));
if(!value.isOpaque()) {
sb.append(toHex((int) (value.getOpacity() * 255)));
}
return sb.toString();
}
private CharSequence toHex(int v) {
StringBuilder sb = new StringBuilder();
if(v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
return sb;
}
@Override
protected void layoutChildren() {
baseColor.resize(44, 25);
accentColor.resize(44, 25);
reset.resize(60, 25);
baseColor.setTranslateX(0);
accentColor.setTranslateX(baseColor.getTranslateX() + baseColor.getWidth() + 10);
reset.setTranslateX(accentColor.getTranslateX() + accentColor.getWidth() + 10);
}
}

View File

@ -0,0 +1,121 @@
package ctbrec.ui.settings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class ListSelectionPane<T extends Comparable<T>> extends GridPane {
private ListView<T> availableListView = new ListView<>();
private ListView<T> selectedListView = new ListView<>();
private Button addModel = new Button(">");
private Button removeModel = new Button("<");
private CheckBox selectAll = new CheckBox("all");
public ListSelectionPane(List<T> available, List<T> selected) {
super();
setHgap(5);
setVgap(5);
createGui();
fillLists(available, selected);
}
private void fillLists(List<T> available, List<T> selected) {
ObservableList<T> obsAvail = FXCollections.observableArrayList(available);
ObservableList<T> obsSel = FXCollections.observableArrayList(selected);
for (Iterator<T> iterator = obsAvail.iterator(); iterator.hasNext();) {
T t = iterator.next();
if(obsSel.contains(t)) {
iterator.remove();
}
}
Collections.sort(obsAvail);
Collections.sort(obsSel);
availableListView.setItems(obsAvail);
selectedListView.setItems(obsSel);
availableListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
selectedListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
private void createGui() {
Label labelAvailable = new Label("Available");
Label labelSelected = new Label("Selected");
add(labelAvailable, 0, 0);
add(availableListView, 0, 1);
VBox buttonBox = new VBox(5);
buttonBox.getChildren().add(addModel);
buttonBox.getChildren().add(removeModel);
buttonBox.setAlignment(Pos.CENTER);
add(buttonBox, 1, 1);
add(labelSelected, 2, 0);
add(selectedListView, 2, 1);
add(selectAll, 0, 2);
GridPane.setHgrow(availableListView, Priority.ALWAYS);
GridPane.setHgrow(selectedListView, Priority.ALWAYS);
GridPane.setFillWidth(availableListView, true);
GridPane.setFillWidth(selectedListView, true);
addModel.setOnAction(evt -> addSelectedItems());
removeModel.setOnAction(evt -> removeSelectedItems());
availableListView.disableProperty().bind(selectAll.selectedProperty());
selectedListView.disableProperty().bind(selectAll.selectedProperty());
addModel.disableProperty().bind(selectAll.selectedProperty());
removeModel.disableProperty().bind(selectAll.selectedProperty());
}
private void addSelectedItems() {
List<T> selected = new ArrayList<>(availableListView.getSelectionModel().getSelectedItems());
for (T t : selected) {
if(!selectedListView.getItems().contains(t)) {
selectedListView.getItems().add(t);
availableListView.getItems().remove(t);
}
}
Collections.sort(selectedListView.getItems());
}
private void removeSelectedItems() {
List<T> selected = new ArrayList<>(selectedListView.getSelectionModel().getSelectedItems());
for (T t : selected) {
if(!availableListView.getItems().contains(t)) {
availableListView.getItems().add(t);
selectedListView.getItems().remove(t);
}
}
Collections.sort(availableListView.getItems());
}
public List<T> getSelectedItems() {
if(selectAll.isSelected()) {
List<T> all = new ArrayList<>(availableListView.getItems());
all.addAll(selectedListView.getItems());
return all;
} else {
return selectedListView.getItems();
}
}
public boolean isAllSelected() {
return selectAll.isSelected();
}
}

View File

@ -1,4 +1,4 @@
package ctbrec.ui;
package ctbrec.ui.settings;
import static ctbrec.Settings.ProxyType.*;
import java.util.ArrayList;
@ -51,18 +51,23 @@ public class ProxySettingsPane extends TitledPane implements EventHandler<Action
l = new Label("Host");
layout.add(l, 0, 1);
layout.add(proxyHost, 1, 1);
proxyHost.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
l = new Label("Port");
layout.add(l, 0, 2);
layout.add(proxyPort, 1, 2);
proxyPort.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
l = new Label("Username");
layout.add(l, 0, 3);
layout.add(proxyUser, 1, 3);
proxyUser.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
l = new Label("Password");
layout.add(l, 0, 4);
layout.add(proxyPassword, 1, 4);
proxyPassword.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
}
private void loadConfig() {
@ -86,6 +91,7 @@ public class ProxySettingsPane extends TitledPane implements EventHandler<Action
public void handle(ActionEvent event) {
setComponentDisableState();
settingsTab.showRestartRequired();
settingsTab.saveConfig();
}
private void setComponentDisableState() {

View File

@ -0,0 +1,581 @@
package ctbrec.ui.settings;
import static ctbrec.Settings.DirectoryStructure.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.Settings.DirectoryStructure;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder;
import ctbrec.sites.ConfigUI;
import ctbrec.sites.Site;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TabSelectionListener;
import ctbrec.ui.controls.DirectorySelectionBox;
import ctbrec.ui.controls.ProgramSelectionBox;
import javafx.collections.FXCollections;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Accordion;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;;
public class SettingsTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
private static final int ONE_GiB_IN_BYTES = 1024 * 1024 * 1024;
public static final int CHECKBOX_MARGIN = 6;
private DirectorySelectionBox recordingsDirectory;
private ProgramSelectionBox mediaPlayer;
private ProgramSelectionBox postProcessing;
private TextField server;
private TextField port;
private TextField onlineCheckIntervalInSecs;
private TextField leaveSpaceOnDevice;
private TextField minimumLengthInSecs;
private CheckBox loadResolution;
private CheckBox secureCommunication = new CheckBox();
private CheckBox chooseStreamQuality = new CheckBox();
private CheckBox multiplePlayers = new CheckBox();
private CheckBox updateThumbnails = new CheckBox();
private CheckBox previewInThumbnails = new CheckBox();
private CheckBox showPlayerStarting = new CheckBox();
private RadioButton recordLocal;
private RadioButton recordRemote;
private ToggleGroup recordLocation;
private ProxySettingsPane proxySettingsPane;
private TextField maxResolution;
private ComboBox<SplitAfterOption> splitAfter;
private ComboBox<DirectoryStructure> directoryStructure;
private ComboBox<String> startTab;
private List<Site> sites;
private Label restartLabel;
private Accordion siteConfigAccordion = new Accordion();
private Recorder recorder;
public SettingsTab(List<Site> sites, Recorder recorder) {
this.sites = sites;
this.recorder = recorder;
setText("Settings");
createGui();
setClosable(false);
setRecordingMode(recordLocal.isSelected());
}
private void createGui() {
// set up main layout, 2 columns with VBoxes 50/50
GridPane mainLayout = createGridLayout();
mainLayout.setHgap(15);
mainLayout.setVgap(15);
mainLayout.setPadding(new Insets(15));
ColumnConstraints cc = new ColumnConstraints();
cc.setPercentWidth(50);
mainLayout.getColumnConstraints().setAll(cc, cc);
ScrollPane scrollPane = new ScrollPane(mainLayout);
setContent(scrollPane);
GridPane.setFillHeight(scrollPane, true);
GridPane.setFillWidth(scrollPane, true);
GridPane.setHgrow(scrollPane, Priority.ALWAYS);
GridPane.setVgrow(scrollPane, Priority.ALWAYS);
VBox leftSide = new VBox(15);
leftSide.setFillWidth(true);
VBox rightSide = new VBox(15);
rightSide.setFillWidth(true);
GridPane.setHgrow(leftSide, Priority.ALWAYS);
GridPane.setHgrow(rightSide, Priority.ALWAYS);
GridPane.setFillWidth(leftSide, true);
GridPane.setFillWidth(rightSide, true);
mainLayout.add(leftSide, 0, 1);
mainLayout.add(rightSide, 1, 1);
mainLayout.prefWidthProperty().bind(scrollPane.widthProperty());
// restart info label
restartLabel = new Label("A restart is required to apply the changes you made!");
restartLabel.setVisible(false);
restartLabel.setFont(Font.font(24));
restartLabel.setTextFill(Color.RED);
mainLayout.add(restartLabel, 0, 0);
GridPane.setColumnSpan(restartLabel, 2);
GridPane.setHalignment(restartLabel, HPos.CENTER);
// left side
leftSide.getChildren().add(createGeneralPanel());
leftSide.getChildren().add(createRecorderPanel());
leftSide.getChildren().add(createRecordLocationPanel());
//right side
rightSide.getChildren().add(siteConfigAccordion);
ActionSettingsPanel actions = new ActionSettingsPanel(this, recorder);
rightSide.getChildren().add(actions);
proxySettingsPane = new ProxySettingsPane(this);
rightSide.getChildren().add(proxySettingsPane);
for (int i = 0; i < sites.size(); i++) {
Site site = sites.get(i);
ConfigUI siteConfig = SiteUiFactory.getUi(site).getConfigUI();
if(siteConfig != null) {
TitledPane pane = new TitledPane(site.getName(), siteConfig.createConfigPanel());
siteConfigAccordion.getPanes().add(pane);
}
}
}
private Node createRecordLocationPanel() {
GridPane layout = createGridLayout();
Label l = new Label("Record Location");
layout.add(l, 0, 0);
recordLocation = new ToggleGroup();
recordLocal = new RadioButton("Local");
recordRemote = new RadioButton("Remote");
recordLocal.setToggleGroup(recordLocation);
recordRemote.setToggleGroup(recordLocation);
recordLocal.setSelected(Config.getInstance().getSettings().localRecording);
recordRemote.setSelected(!recordLocal.isSelected());
layout.add(recordLocal, 1, 0);
layout.add(recordRemote, 2, 0);
recordLocation.selectedToggleProperty().addListener((e) -> {
Config.getInstance().getSettings().localRecording = recordLocal.isSelected();
setRecordingMode(recordLocal.isSelected());
showRestartRequired();
saveConfig();
});
GridPane.setMargin(l, new Insets(0, 0, CHECKBOX_MARGIN, 0));
GridPane.setMargin(recordLocal, new Insets(0, 0, CHECKBOX_MARGIN, 0));
GridPane.setMargin(recordRemote, new Insets(0, 0, CHECKBOX_MARGIN, 0));
layout.add(new Label("Server"), 0, 1);
server = new TextField(Config.getInstance().getSettings().httpServer);
server.textProperty().addListener((ob, o, n) -> {
if(!server.getText().isEmpty()) {
Config.getInstance().getSettings().httpServer = server.getText();
saveConfig();
}
});
GridPane.setFillWidth(server, true);
GridPane.setHgrow(server, Priority.ALWAYS);
GridPane.setColumnSpan(server, 2);
layout.add(server, 1, 1);
layout.add(new Label("Port"), 0, 2);
port = new TextField(Integer.toString(Config.getInstance().getSettings().httpPort));
port.textProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
port.setText(newValue.replaceAll("[^\\d]", ""));
}
if(!port.getText().isEmpty()) {
Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText());
saveConfig();
}
});
GridPane.setFillWidth(port, true);
GridPane.setHgrow(port, Priority.ALWAYS);
GridPane.setColumnSpan(port, 2);
layout.add(port, 1, 2);
l = new Label("Require authentication");
layout.add(l, 0, 3);
secureCommunication.setSelected(Config.getInstance().getSettings().requireAuthentication);
secureCommunication.setOnAction((e) -> {
Config.getInstance().getSettings().requireAuthentication = secureCommunication.isSelected();
if(secureCommunication.isSelected()) {
byte[] key = Config.getInstance().getSettings().key;
if(key == null) {
key = Hmac.generateKey();
Config.getInstance().getSettings().key = key;
saveConfig();
}
TextInputDialog keyDialog = new TextInputDialog();
keyDialog.setResizable(true);
keyDialog.setTitle("Server Authentication");
keyDialog.setHeaderText("A key has been generated");
keyDialog.setContentText("Add this setting to your server's config.json:\n");
keyDialog.getEditor().setText("\"key\": " + Arrays.toString(key));
keyDialog.getEditor().setEditable(false);
keyDialog.setWidth(800);
keyDialog.setHeight(200);
keyDialog.show();
}
});
GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0));
GridPane.setMargin(secureCommunication, new Insets(4, 0, 0, 0));
layout.add(secureCommunication, 1, 3);
TitledPane recordLocation = new TitledPane("Record Location", layout);
recordLocation.setCollapsible(false);
return recordLocation;
}
private Node createRecorderPanel() {
int row = 0;
GridPane layout = createGridLayout();
layout.add(new Label("Recordings Directory"), 0, row);
recordingsDirectory = new DirectorySelectionBox(Config.getInstance().getSettings().recordingsDir);
recordingsDirectory.prefWidth(400);
recordingsDirectory.fileProperty().addListener((obs, o, n) -> {
String path = n;
if(!Objects.equals(path, Config.getInstance().getSettings().recordingsDir)) {
Config.getInstance().getSettings().recordingsDir = path;
saveConfig();
}
});
GridPane.setFillWidth(recordingsDirectory, true);
GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS);
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(recordingsDirectory, 1, row++);
layout.add(new Label("Directory Structure"), 0, row);
List<DirectoryStructure> options = new ArrayList<>();
options.add(FLAT);
options.add(ONE_PER_MODEL);
options.add(ONE_PER_RECORDING);
directoryStructure = new ComboBox<>(FXCollections.observableList(options));
directoryStructure.setValue(Config.getInstance().getSettings().recordingsDirStructure);
directoryStructure.setOnAction((evt) -> {
Config.getInstance().getSettings().recordingsDirStructure = directoryStructure.getValue();
saveConfig();
});
GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(directoryStructure, 1, row++);
recordingsDirectory.prefWidthProperty().bind(directoryStructure.widthProperty());
Label l = new Label("Split recordings after (minutes)");
layout.add(l, 0, row);
List<SplitAfterOption> splitOptions = new ArrayList<>();
splitOptions.add(new SplitAfterOption("disabled", 0));
if(Config.isDevMode()) {
splitOptions.add(new SplitAfterOption( "1 min", 1 * 60));
splitOptions.add(new SplitAfterOption( "3 min", 3 * 60));
splitOptions.add(new SplitAfterOption( "5 min", 5 * 60));
}
splitOptions.add(new SplitAfterOption("10 min", 10 * 60));
splitOptions.add(new SplitAfterOption("15 min", 15 * 60));
splitOptions.add(new SplitAfterOption("20 min", 20 * 60));
splitOptions.add(new SplitAfterOption("30 min", 30 * 60));
splitOptions.add(new SplitAfterOption("60 min", 60 * 60));
splitAfter = new ComboBox<>(FXCollections.observableList(splitOptions));
layout.add(splitAfter, 1, row++);
setSplitAfterValue();
splitAfter.setOnAction((e) -> {
Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue();
saveConfig();
});
splitAfter.prefWidthProperty().bind(directoryStructure.widthProperty());
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
l = new Label("Maximum resolution (0 = unlimited)");
layout.add(l, 0, row);
maxResolution = new TextField(Integer.toString(Config.getInstance().getSettings().maximumResolution));
maxResolution.textProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
maxResolution.setText(newValue.replaceAll("[^\\d]", ""));
}
if (!maxResolution.getText().isEmpty()) {
int newRes = Integer.parseInt(maxResolution.getText());
if (newRes != Config.getInstance().getSettings().maximumResolution) {
Config.getInstance().getSettings().maximumResolution = newRes;
saveConfig();
}
}
});
maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty());
layout.add(maxResolution, 1, row++);
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(new Label("Post-Processing"), 0, row);
postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing);
postProcessing.allowEmptyValue();
postProcessing.fileProperty().addListener((obs, o, n) -> {
String path = n;
if(!Objects.equals(path, Config.getInstance().getSettings().postProcessing)) {
Config.getInstance().getSettings().postProcessing = path;
saveConfig();
}
});
GridPane.setFillWidth(postProcessing, true);
GridPane.setHgrow(postProcessing, Priority.ALWAYS);
GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(postProcessing, 1, row++);
Tooltip tt = new Tooltip("Check every x seconds, if a model came online");
l = new Label("Check online state every (seconds)");
l.setTooltip(tt);
layout.add(l, 0, row);
onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs));
onlineCheckIntervalInSecs.setTooltip(tt);
onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
onlineCheckIntervalInSecs.setText(newValue.replaceAll("[^\\d]", ""));
}
if(!onlineCheckIntervalInSecs.getText().isEmpty()) {
Config.getInstance().getSettings().onlineCheckIntervalInSecs = Integer.parseInt(onlineCheckIntervalInSecs.getText());
saveConfig();
}
});
GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(onlineCheckIntervalInSecs, 1, row++);
tt = new Tooltip("Stop recording, if the free space on the device gets below this threshold");
l = new Label("Leave space on device (GiB)");
l.setTooltip(tt);
layout.add(l, 0, row);
long minimumSpaceLeftInBytes = Config.getInstance().getSettings().minimumSpaceLeftInBytes;
int minimumSpaceLeftInGiB = (int) (minimumSpaceLeftInBytes / ONE_GiB_IN_BYTES);
leaveSpaceOnDevice = new TextField(Integer.toString(minimumSpaceLeftInGiB));
leaveSpaceOnDevice.setTooltip(tt);
leaveSpaceOnDevice.textProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
leaveSpaceOnDevice.setText(newValue.replaceAll("[^\\d]", ""));
}
if(!leaveSpaceOnDevice.getText().isEmpty()) {
long spaceLeftInGiB = Long.parseLong(leaveSpaceOnDevice.getText());
Config.getInstance().getSettings().minimumSpaceLeftInBytes = spaceLeftInGiB * ONE_GiB_IN_BYTES;
saveConfig();
}
});
GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(leaveSpaceOnDevice, 1, row++);
tt = new Tooltip("Delete recordings, which are shorter than x seconds. 0 to disable.");
l = new Label("Delete recordings shorter than (secs)");
l.setTooltip(tt);
layout.add(l, 0, row);
int minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds;
minimumLengthInSecs = new TextField(Integer.toString(minimumLengthInSeconds));
minimumLengthInSecs.setTooltip(tt);
minimumLengthInSecs.textProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
minimumLengthInSecs.setText(newValue.replaceAll("[^\\d]", ""));
}
if(!minimumLengthInSecs.getText().isEmpty()) {
int minimumLength = Integer.parseInt(minimumLengthInSecs.getText());
Config.getInstance().getSettings().minimumLengthInSeconds = minimumLength;
saveConfig();
}
});
GridPane.setMargin(minimumLengthInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(minimumLengthInSecs, 1, row++);
TitledPane locations = new TitledPane("Recorder", layout);
locations.setCollapsible(false);
return locations;
}
private Node createGeneralPanel() {
GridPane layout = createGridLayout();
int row = 0;
layout.add(new Label("Player"), 0, row);
mediaPlayer = new ProgramSelectionBox(Config.getInstance().getSettings().mediaPlayer);
mediaPlayer.fileProperty().addListener((obs, o, n) -> {
String path = n;
if (!Objects.equals(path, Config.getInstance().getSettings().mediaPlayer)) {
Config.getInstance().getSettings().mediaPlayer = path;
saveConfig();
}
});
GridPane.setFillWidth(mediaPlayer, true);
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(mediaPlayer, 1, row++);
Label l = new Label("Allow multiple players");
layout.add(l, 0, row);
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
multiplePlayers.setOnAction((e) -> {
Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(multiplePlayers, 1, row++);
l = new Label("Show \"Player Starting\" Message");
layout.add(l, 0, row);
showPlayerStarting.setSelected(Config.getInstance().getSettings().showPlayerStarting);
showPlayerStarting.setOnAction((e) -> {
Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(showPlayerStarting, 1, row++);
l = new Label("Display stream resolution in overview");
layout.add(l, 0, row);
loadResolution = new CheckBox();
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
loadResolution.setOnAction((e) -> {
Config.getInstance().getSettings().determineResolution = loadResolution.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(loadResolution, 1, row++);
l = new Label("Manually select stream quality");
layout.add(l, 0, row);
chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality);
chooseStreamQuality.setOnAction((e) -> {
Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(chooseStreamQuality, 1, row++);
l = new Label("Update thumbnails");
layout.add(l, 0, row);
updateThumbnails.setSelected(Config.getInstance().getSettings().updateThumbnails);
updateThumbnails.setOnAction((e) -> {
Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(updateThumbnails, 1, row++);
l = new Label("Preview in thumbnails");
layout.add(l, 0, row);
previewInThumbnails.setSelected(Config.getInstance().getSettings().previewInThumbnails);
previewInThumbnails.setOnAction((e) -> {
Config.getInstance().getSettings().previewInThumbnails = previewInThumbnails.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(previewInThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(previewInThumbnails, 1, row++);
l = new Label("Start Tab");
layout.add(l, 0, row);
startTab = new ComboBox<>();
startTab.setOnAction((e) -> {
Config.getInstance().getSettings().startTab = startTab.getSelectionModel().getSelectedItem();
saveConfig();
});
layout.add(startTab, 1, row++);
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(startTab, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
l = new Label("Colors (Base / Accent)");
layout.add(l, 0, row);
ColorSettingsPane colorSettingsPane = new ColorSettingsPane(this);
layout.add(colorSettingsPane, 1, row++);
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
TitledPane general = new TitledPane("General", layout);
general.setCollapsible(false);
return general;
}
private void setSplitAfterValue() {
int value = Config.getInstance().getSettings().splitRecordings;
for (SplitAfterOption option : splitAfter.getItems()) {
if(option.getValue() == value) {
splitAfter.getSelectionModel().select(option);
}
}
}
void showRestartRequired() {
restartLabel.setVisible(true);
}
public static GridPane createGridLayout() {
GridPane layout = new GridPane();
layout.setPadding(new Insets(10));
layout.setHgap(5);
layout.setVgap(5);
return layout;
}
private void setRecordingMode(boolean local) {
server.setDisable(local);
port.setDisable(local);
secureCommunication.setDisable(local);
recordingsDirectory.setDisable(!local);
splitAfter.setDisable(!local);
maxResolution.setDisable(!local);
directoryStructure.setDisable(!local);
onlineCheckIntervalInSecs.setDisable(!local);
leaveSpaceOnDevice.setDisable(!local);
postProcessing.setDisable(!local);
}
@Override
public void selected() {
if(startTab.getItems().isEmpty()) {
for(Tab tab : getTabPane().getTabs()) {
startTab.getItems().add(tab.getText());
}
}
String startTabName = Config.getInstance().getSettings().startTab;
if(StringUtil.isNotBlank(startTabName)) {
startTab.getSelectionModel().select(startTabName);
}
}
@Override
public void deselected() {
saveConfig();
}
public void saveConfig() {
if(proxySettingsPane != null) {
proxySettingsPane.saveConfig();
}
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.error("Couldn't save config", e);
}
}
public static class SplitAfterOption {
private String label;
private int value;
public SplitAfterOption(String label, int value) {
super();
this.label = label;
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return label;
}
}
}

View File

@ -0,0 +1,22 @@
package ctbrec.ui.sites;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
public abstract class AbstractConfigUI implements ConfigUI {
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractConfigUI.class);
protected void save() {
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.error("Couldn't save config");
}
}
}

View File

@ -1,21 +1,22 @@
package ctbrec.ui.sites.bonga;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.Settings;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SettingsTab;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class BongaCamsConfigUI implements ConfigUI {
public class BongaCamsConfigUI extends AbstractConfigUI {
private BongaCams bongaCams;
public BongaCamsConfigUI(BongaCams bongaCams) {
@ -25,26 +26,56 @@ public class BongaCamsConfigUI implements ConfigUI {
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("BongaCams User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().bongaUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaUsername = username.getText());
Settings settings = Config.getInstance().getSettings();
int row = 0;
Label l = new Label("Active");
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(bongaCams.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(bongaCams.getName());
} else {
settings.disabledSites.add(bongaCams.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("BongaCams User"), 0, row);
TextField username = new TextField(settings.bongaUsername);
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().bongaUsername)) {
Config.getInstance().getSettings().bongaUsername = username.getText();
bongaCams.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(username, 1, row++);
layout.add(new Label("BongaCams Password"), 0, 1);
layout.add(new Label("BongaCams Password"), 0, row);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().bongaPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaPassword = password.getText());
password.setText(settings.bongaPassword);
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().bongaPassword)) {
Config.getInstance().getSettings().bongaPassword = password.getText();
bongaCams.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
layout.add(password, 1, row++);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntegration.open(bongaCams.getAffiliateLink()));
layout.add(createAccount, 1, 2);
layout.add(createAccount, 1, row++);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));

View File

@ -44,7 +44,7 @@ public class BongaCamsSiteUi implements SiteUI {
}
@Override
public boolean login() throws IOException {
public synchronized boolean login() throws IOException {
boolean automaticLogin = bongaCams.login();
if(automaticLogin) {
return true;

View File

@ -1,5 +1,7 @@
package ctbrec.ui.sites.bonga;
import static ctbrec.Model.State.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -54,22 +56,41 @@ public class BongaCamsUpdateService extends PaginatedScheduledService {
JSONArray _models = json.getJSONArray("models");
for (int i = 0; i < _models.length(); i++) {
JSONObject m = _models.getJSONObject(i);
String name = m.getString("username");
String name = m.optString("username");
if(name.isEmpty()) {
continue;
}
BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name);
model.setUserId(m.getInt("user_id"));
boolean away = m.optBoolean("is_away");
boolean online = m.optBoolean("online") && !away;
boolean online = m.optBoolean("online");
model.setOnline(online);
if(online) {
model.setOnlineState(ONLINE);
if(away) {
model.setOnlineState("away");
model.setOnlineState(AWAY);
} else {
model.setOnlineState(m.getString("room"));
switch(m.optString("room")) {
case "private":
case "fullprivate":
model.setOnlineState(PRIVATE);
break;
case "group":
case "public":
model.setOnlineState(ONLINE);
break;
default:
LOG.debug(m.optString("room"));
model.setOnlineState(ONLINE);
}
}
} else {
model.setOnlineState("offline");
model.setOnlineState(OFFLINE);
}
model.setPreview("https:" + m.getString("thumb_image"));
if(m.has("display_name")) {
model.setDisplayName(m.getString("display_name"));
}
models.add(model);
}
}

View File

@ -1,44 +1,81 @@
package ctbrec.ui.sites.cam4;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.Settings;
import ctbrec.sites.cam4.Cam4;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SettingsTab;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class Cam4ConfigUI implements ConfigUI {
public class Cam4ConfigUI extends AbstractConfigUI {
private Cam4 cam4;
public Cam4ConfigUI(Cam4 cam4) {
this.cam4 = cam4;
}
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("Cam4 User"), 0, 0);
Settings settings = Config.getInstance().getSettings();
int row = 0;
Label l = new Label("Active");
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(cam4.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(cam4.getName());
} else {
settings.disabledSites.add(cam4.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("Cam4 User"), 0, row);
TextField username = new TextField(Config.getInstance().getSettings().cam4Username);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().cam4Username = username.getText());
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().cam4Username)) {
Config.getInstance().getSettings().cam4Username = username.getText();
cam4.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(username, 1, row++);
layout.add(new Label("Cam4 Password"), 0, 1);
layout.add(new Label("Cam4 Password"), 0, row);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().cam4Password);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().cam4Password = password.getText());
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().cam4Password)) {
Config.getInstance().getSettings().cam4Password = password.getText();
cam4.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
layout.add(password, 1, row++);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntegration.open(Cam4.AFFILIATE_LINK));
layout.add(createAccount, 1, 2);
layout.add(createAccount, 1, row++);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));

View File

@ -68,7 +68,7 @@ public class Cam4FollowedUpdateService extends PaginatedScheduledService {
String modelName = path.substring(1);
Cam4Model model = (Cam4Model) site.createModel(modelName);
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis());
model.setOnlineState(parseOnlineState(cellHtml));
model.setOnlineStateByShowType(parseOnlineState(cellHtml));
models.add(model);
}
return models.stream()

View File

@ -30,7 +30,7 @@ public class Cam4SiteUi implements SiteUI {
public Cam4SiteUi(Cam4 cam4) {
this.cam4 = cam4;
tabProvider = new Cam4TabProvider(cam4);
configUI = new Cam4ConfigUI();
configUI = new Cam4ConfigUI(cam4);
}
@Override
@ -44,7 +44,7 @@ public class Cam4SiteUi implements SiteUI {
}
@Override
public boolean login() throws IOException {
public synchronized boolean login() throws IOException {
boolean automaticLogin = cam4.login();
if(automaticLogin) {
return true;

View File

@ -1,21 +1,22 @@
package ctbrec.ui.sites.camsoda;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.Settings;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SettingsTab;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class CamsodaConfigUI implements ConfigUI {
public class CamsodaConfigUI extends AbstractConfigUI {
private Camsoda camsoda;
public CamsodaConfigUI(Camsoda camsoda) {
@ -25,26 +26,56 @@ public class CamsodaConfigUI implements ConfigUI {
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("CamSoda User"), 0, 0);
Settings settings = Config.getInstance().getSettings();
int row = 0;
Label l = new Label("Active");
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(camsoda.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(camsoda.getName());
} else {
settings.disabledSites.add(camsoda.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("CamSoda User"), 0, row);
TextField username = new TextField(Config.getInstance().getSettings().camsodaUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaUsername = username.getText());
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().camsodaUsername)) {
Config.getInstance().getSettings().camsodaUsername = username.getText();
camsoda.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(username, 1, row++);
layout.add(new Label("CamSoda Password"), 0, 1);
layout.add(new Label("CamSoda Password"), 0, row);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().camsodaPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaPassword = password.getText());
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().camsodaPassword)) {
Config.getInstance().getSettings().camsodaPassword = password.getText();
camsoda.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
layout.add(password, 1, row++);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntegration.open(camsoda.getAffiliateLink()));
layout.add(createAccount, 1, 2);
layout.add(createAccount, 1, row++);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));

View File

@ -1,5 +1,7 @@
package ctbrec.ui.sites.camsoda;
import static ctbrec.Model.State.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@ -47,7 +49,7 @@ public class CamsodaFollowedUpdateService extends PaginatedScheduledService {
JSONObject m = following.getJSONObject(i);
CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname"));
boolean online = m.getInt("online") == 1;
model.setOnlineState(online ? "online" : "offline");
model.setOnlineState(online ? ONLINE : OFFLINE);
model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg");
models.add(model);
}

View File

@ -45,7 +45,7 @@ public class CamsodaSiteUi implements SiteUI {
}
@Override
public boolean login() throws IOException {
public synchronized boolean login() throws IOException {
boolean automaticLogin = camsoda.login();
return automaticLogin;
}

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.Collectors;
import org.json.JSONArray;
@ -56,39 +58,43 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(json.has("status") && json.getBoolean("status")) {
JSONArray template = json.getJSONArray("template");
JSONArray results = json.getJSONArray("results");
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
if(result.has("tpl")) {
JSONArray tpl = result.getJSONArray("tpl");
String name = tpl.getString(0);
String name = tpl.getString(getTemplateIndex(template, "username"));
String displayName = tpl.getString(getTemplateIndex(template, "display_name"));
// int connections = tpl.getInt(2);
String streamName = tpl.getString(5);
String tsize = tpl.getString(6);
String serverPrefix = tpl.getString(7);
String streamName = tpl.getString(getTemplateIndex(template, "stream_name"));
String tsize = tpl.getString(getTemplateIndex(template, "tsize"));
String serverPrefix = tpl.getString(getTemplateIndex(template, "server_prefix"));
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
model.setDescription(tpl.getString(4));
model.setSortOrder(tpl.getFloat(3));
model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html")));
model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value")));
long unixtime = System.currentTimeMillis() / 1000;
String preview = "https://thumbs-orig.camsoda.com/thumbs/"
+ streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime;
model.setPreview(preview);
if(result.has("edge_servers")) {
JSONArray edgeServers = result.getJSONArray("edge_servers");
JSONArray edgeServers = tpl.getJSONArray(getTemplateIndex(template, "edge_servers"));
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
}
model.setDisplayName(displayName);
models.add(model);
} else {
String name = result.getString("username");
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
if(result.has("server_prefix")) {
String serverPrefix = result.getString("server_prefix");
String streamName = result.getString("stream_name");
model.setSortOrder(result.getFloat("sort_value"));
models.add(model);
if(result.has("status")) {
model.setOnlineState(result.getString("status"));
model.setOnlineStateByStatus(result.getString("status"));
}
if(result.has("display_name")) {
model.setDisplayName(result.getString("display_name"));
}
if(result.has("edge_servers")) {
@ -120,6 +126,16 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
}
}
}
private int getTemplateIndex(JSONArray template, String string) {
for (int i = 0; i < template.length(); i++) {
String s = template.getString(i);
if(Objects.equals(s, string)) {
return i;
}
}
throw new NoSuchElementException(string + " not found in template: " + template.toString());
}
};
}

View File

@ -1,49 +1,101 @@
package ctbrec.ui.sites.chaturbate;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.Settings;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SettingsTab;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class ChaturbateConfigUi implements ConfigUI {
public class ChaturbateConfigUi extends AbstractConfigUI {
private Chaturbate chaturbate;
public ChaturbateConfigUi(Chaturbate chaturbate) {
this.chaturbate = chaturbate;
}
@Override
public Parent createConfigPanel() {
Settings settings = Config.getInstance().getSettings();
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("Chaturbate User"), 0, 0);
int row = 0;
Label l = new Label("Active");
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(chaturbate.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(chaturbate.getName());
} else {
settings.disabledSites.add(chaturbate.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("Chaturbate User"), 0, row);
TextField username = new TextField(Config.getInstance().getSettings().username);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().username = username.getText());
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().username)) {
Config.getInstance().getSettings().username = n;
chaturbate.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(username, 1, row++);
layout.add(new Label("Chaturbate Password"), 0, 1);
layout.add(new Label("Chaturbate Password"), 0, row);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().password);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().password = password.getText());
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().password)) {
Config.getInstance().getSettings().password = n;
chaturbate.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
layout.add(password, 1, row++);
layout.add(new Label("Chaturbate Base URL"), 0, row);
TextField baseUrl = new TextField();
baseUrl.setText(Config.getInstance().getSettings().chaturbateBaseUrl);
baseUrl.textProperty().addListener((ob, o, n) -> {
Config.getInstance().getSettings().chaturbateBaseUrl = baseUrl.getText();
save();
});
GridPane.setFillWidth(baseUrl, true);
GridPane.setHgrow(baseUrl, Priority.ALWAYS);
GridPane.setColumnSpan(baseUrl, 2);
layout.add(baseUrl, 1, row++);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntegration.open(Chaturbate.REGISTRATION_LINK));
layout.add(createAccount, 1, 2);
layout.add(createAccount, 1, row++);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(baseUrl, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
username.setPrefWidth(300);
return layout;
}
}

View File

@ -16,7 +16,7 @@ public class ChaturbateSiteUi implements SiteUI {
public ChaturbateSiteUi(Chaturbate chaturbate) {
this.chaturbate = chaturbate;
tabProvider = new ChaturbateTabProvider(chaturbate);
configUi = new ChaturbateConfigUi();
configUi = new ChaturbateConfigUi(chaturbate);
}
@Override
@ -30,7 +30,7 @@ public class ChaturbateSiteUi implements SiteUI {
}
@Override
public boolean login() throws IOException {
public synchronized boolean login() throws IOException {
return chaturbate.login();
}

View File

@ -1,7 +1,5 @@
package ctbrec.ui.sites.chaturbate;
import static ctbrec.sites.chaturbate.Chaturbate.*;
import java.util.ArrayList;
import java.util.List;
@ -21,17 +19,17 @@ public class ChaturbateTabProvider extends TabProvider {
public ChaturbateTabProvider(Chaturbate chaturbate) {
this.chaturbate = chaturbate;
this.recorder = chaturbate.getRecorder();
this.followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate);
this.followedTab = new ChaturbateFollowedTab("Followed", chaturbate.getBaseUrl() + "/followed-cams/", chaturbate);
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Featured", BASE_URI + "/"));
tabs.add(createTab("Female", BASE_URI + "/female-cams/"));
tabs.add(createTab("Male", BASE_URI + "/male-cams/"));
tabs.add(createTab("Couples", BASE_URI + "/couple-cams/"));
tabs.add(createTab("Trans", BASE_URI + "/trans-cams/"));
tabs.add(createTab("Featured", chaturbate.getBaseUrl() + "/"));
tabs.add(createTab("Female", chaturbate.getBaseUrl() + "/female-cams/"));
tabs.add(createTab("Male", chaturbate.getBaseUrl() + "/male-cams/"));
tabs.add(createTab("Couples", chaturbate.getBaseUrl() + "/couple-cams/"));
tabs.add(createTab("Trans", chaturbate.getBaseUrl() + "/trans-cams/"));
followedTab.setScene(scene);
followedTab.setRecorder(recorder);
tabs.add(followedTab);

View File

@ -1,21 +1,22 @@
package ctbrec.ui.sites.myfreecams;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.Settings;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SettingsTab;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class MyFreeCamsConfigUI implements ConfigUI {
public class MyFreeCamsConfigUI extends AbstractConfigUI {
private MyFreeCams myFreeCams;
public MyFreeCamsConfigUI(MyFreeCams myFreeCams) {
@ -24,32 +25,85 @@ public class MyFreeCamsConfigUI implements ConfigUI {
@Override
public Parent createConfigPanel() {
int row = 0;
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("MyFreeCams User"), 0, 0);
Settings settings = Config.getInstance().getSettings();
Label l = new Label("Active");
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(myFreeCams.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(myFreeCams.getName());
} else {
settings.disabledSites.add(myFreeCams.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("MyFreeCams User"), 0, row);
TextField username = new TextField(Config.getInstance().getSettings().mfcUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcUsername = username.getText());
username.setPrefWidth(300);
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().mfcUsername)) {
Config.getInstance().getSettings().mfcUsername = username.getText();
myFreeCams.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(username, 1, row++);
layout.add(new Label("MyFreeCams Password"), 0, 1);
layout.add(new Label("MyFreeCams Password"), 0, row);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().mfcPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcPassword = password.getText());
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().mfcPassword)) {
Config.getInstance().getSettings().mfcPassword = password.getText();
myFreeCams.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
layout.add(password, 1, row++);
layout.add(new Label("MyFreeCams Base URL"), 0, row);
TextField baseUrl = new TextField();
baseUrl.setText(Config.getInstance().getSettings().mfcBaseUrl);
baseUrl.textProperty().addListener((ob, o, n) -> {
Config.getInstance().getSettings().mfcBaseUrl = baseUrl.getText();
save();
});
GridPane.setFillWidth(baseUrl, true);
GridPane.setHgrow(baseUrl, Priority.ALWAYS);
GridPane.setColumnSpan(baseUrl, 2);
layout.add(baseUrl, 1, row++);
layout.add(new Label("Ignore upscaled stream (960p)"), 0, row);
CheckBox ignoreUpscaled = new CheckBox();
ignoreUpscaled.setSelected(Config.getInstance().getSettings().mfcIgnoreUpscaled);
ignoreUpscaled.selectedProperty().addListener((obs, oldV, newV) -> {
Config.getInstance().getSettings().mfcIgnoreUpscaled = newV;
});
layout.add(ignoreUpscaled, 1, row++);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntegration.open(myFreeCams.getAffiliateLink()));
layout.add(createAccount, 1, 2);
layout.add(createAccount, 1, row);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(baseUrl, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(ignoreUpscaled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
return layout;
}
}

View File

@ -30,7 +30,7 @@ public class MyFreeCamsSiteUi implements SiteUI {
}
@Override
public boolean login() throws IOException {
public synchronized boolean login() throws IOException {
return myFreeCams.login();
}

View File

@ -49,6 +49,8 @@ public class MyFreeCamsTabProvider extends TabProvider {
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10)));
tabs.add(pop);
MyFreeCamsTableTab table = new MyFreeCamsTableTab(myFreeCams);
tabs.add(table);
return tabs;
}

View File

@ -0,0 +1,568 @@
package ctbrec.ui.sites.myfreecams;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.mfc.MyFreeCamsModel;
import ctbrec.sites.mfc.SessionState;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.TabSelectionListener;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.StartRecordingAction;
import ctbrec.ui.controls.SearchBox;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker.State;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Duration;
public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsTableTab.class);
private ScrollPane scrollPane = new ScrollPane();
private TableView<ModelTableRow> table = new TableView<ModelTableRow>();
private ObservableList<ModelTableRow> filteredModels = FXCollections.observableArrayList();
private ObservableList<ModelTableRow> observableModels = FXCollections.observableArrayList();
private TableUpdateService updateService;
private MyFreeCams mfc;
private ReentrantLock lock = new ReentrantLock();
private SearchBox filterInput;
private Label count = new Label("models");
private List<TableColumn<ModelTableRow, ?>> columns = new ArrayList<>();
private ContextMenu popup;
public MyFreeCamsTableTab(MyFreeCams mfc) {
this.mfc = mfc;
setText("Tabular");
setClosable(false);
initUpdateService();
createGui();
restoreState();
filter(filterInput.getText());
}
private void initUpdateService() {
updateService = new TableUpdateService(mfc);
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(1)));
updateService.setOnSucceeded(this::onSuccess);
updateService.setOnFailed((event) -> {
LOG.info("Couldn't update MyFreeCams model table", event.getSource().getException());
});
}
private void onSuccess(WorkerStateEvent evt) {
Collection<SessionState> sessionStates = updateService.getValue();
if (sessionStates == null) {
return;
}
lock.lock();
try {
for (SessionState updatedModel : sessionStates) {
ModelTableRow row = new ModelTableRow(updatedModel);
int index = observableModels.indexOf(row);
if (index == -1) {
observableModels.add(row);
} else {
observableModels.get(index).update(updatedModel);
}
}
for (Iterator<ModelTableRow> iterator = observableModels.iterator(); iterator.hasNext();) {
ModelTableRow model = iterator.next();
boolean found = false;
for (SessionState sessionState : sessionStates) {
if(Objects.equals(sessionState.getUid(), model.uid)) {
found = true;
break;
}
}
if(!found) {
iterator.remove();
}
}
} finally {
lock.unlock();
}
filteredModels.clear();
filter(filterInput.getText());
table.sort();
}
private void createGui() {
BorderPane layout = new BorderPane();
layout.setPadding(new Insets(5, 10, 10, 10));
filterInput = new SearchBox(false);
filterInput.setPromptText("Filter");
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
String filter = filterInput.getText();
Config.getInstance().getSettings().mfcModelsTableFilter = filter;
lock.lock();
try {
filter(filter);
} finally {
lock.unlock();
}
});
filterInput.getStyleClass().remove("search-box-icon");
HBox.setHgrow(filterInput, Priority.ALWAYS);
Button columnSelection = new Button("");
//Button columnSelection = new Button("");
columnSelection.setOnAction(this::showColumnSelection);
HBox topBar = new HBox(5);
topBar.getChildren().addAll(filterInput, count, columnSelection);
count.prefHeightProperty().bind(filterInput.heightProperty());
count.setAlignment(Pos.CENTER);
layout.setTop(topBar);
BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0));
table.setItems(observableModels);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.getSortOrder().addListener(createSortOrderChangedListener());
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
popup = createContextMenu();
if (popup != null) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (popup != null) {
popup.hide();
}
});
int idx = 0;
TableColumn<ModelTableRow, String> name = createTableColumn("Name", 200, idx++);
name.setCellValueFactory(cdf -> cdf.getValue().nameProperty());
addTableColumnIfEnabled(name);
TableColumn<ModelTableRow, String> state = createTableColumn("State", 130, idx++);
state.setCellValueFactory(cdf -> cdf.getValue().stateProperty());
addTableColumnIfEnabled(state);
TableColumn<ModelTableRow, Number> camscore = createTableColumn("Score", 75, idx++);
camscore.setCellValueFactory(cdf -> cdf.getValue().camScoreProperty());
addTableColumnIfEnabled(camscore);
// this is always 0, use https://api.myfreecams.com/missmfc and https://api.myfreecams.com/missmfc/online
// TableColumn<SessionState, Number> missMfc = createTableColumn("Miss MFC", 75, idx++);
// missMfc.setCellValueFactory(cdf -> {
// Integer mmfc = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getMissmfc()).orElse(-1);
// return new SimpleIntegerProperty(mmfc);
// });
// addTableColumnIfEnabled(missMfc);
TableColumn<ModelTableRow, String> newModel = createTableColumn("New", 60, idx++);
newModel.setCellValueFactory(cdf -> cdf.getValue().newModelProperty());
addTableColumnIfEnabled(newModel);
TableColumn<ModelTableRow, String> ethnic = createTableColumn("Ethnicity", 130, idx++);
ethnic.setCellValueFactory(cdf -> cdf.getValue().ethnicityProperty());
addTableColumnIfEnabled(ethnic);
TableColumn<ModelTableRow, String> country = createTableColumn("Country", 160, idx++);
country.setCellValueFactory(cdf -> cdf.getValue().countryProperty());
addTableColumnIfEnabled(country);
TableColumn<ModelTableRow, String> continent = createTableColumn("Continent", 100, idx++);
continent.setCellValueFactory(cdf -> cdf.getValue().continentProperty());
addTableColumnIfEnabled(continent);
TableColumn<ModelTableRow, String> occupation = createTableColumn("Occupation", 160, idx++);
occupation.setCellValueFactory(cdf -> cdf.getValue().occupationProperty());
addTableColumnIfEnabled(occupation);
TableColumn<ModelTableRow, String> tags = createTableColumn("Tags", 300, idx++);
tags.setCellValueFactory(cdf -> cdf.getValue().tagsProperty());
addTableColumnIfEnabled(tags);
TableColumn<ModelTableRow, String> blurp = createTableColumn("Blurp", 300, idx++);
blurp.setCellValueFactory(cdf -> cdf.getValue().blurpProperty());
addTableColumnIfEnabled(blurp);
TableColumn<ModelTableRow, String> topic = createTableColumn("Topic", 600, idx++);
topic.setCellValueFactory(cdf -> cdf.getValue().topicProperty());
addTableColumnIfEnabled(topic);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setContent(table);
scrollPane.setStyle("-fx-background-color: -fx-background");
layout.setCenter(scrollPane);
setContent(layout);
}
private ContextMenu createContextMenu() {
ObservableList<ModelTableRow> selectedStates = table.getSelectionModel().getSelectedItems();
if (selectedStates.isEmpty()) {
return null;
}
List<Model> selectedModels = new ArrayList<>();
for (ModelTableRow sessionState : selectedStates) {
if(sessionState.name.get() != null) {
MyFreeCamsModel model = mfc.createModel(sessionState.name.get());
mfc.getClient().update(model);
selectedModels.add(model);
}
}
MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction((e) -> {
Model selected = selectedModels.get(0);
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString(selected.getUrl());
clipboard.setContent(content);
});
MenuItem startRecording = new MenuItem("Start Recording");
startRecording.setOnAction((e) -> startRecording(selectedModels));
MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0)));
MenuItem follow = new MenuItem("Follow");
follow.setOnAction((e) -> new FollowAction(getTabPane(), selectedModels).execute());
ContextMenu menu = new ContextMenu();
menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow);
if (selectedModels.size() > 1) {
copyUrl.setDisable(true);
openInPlayer.setDisable(true);
openInBrowser.setDisable(true);
}
return menu;
}
private void startRecording(List<Model> selectedModels) {
new StartRecordingAction(getTabPane(), selectedModels, mfc.getRecorder()).execute();
}
private void openInPlayer(Model selectedModel) {
new PlayAction(getTabPane(), selectedModel).execute();
}
private void addTableColumnIfEnabled(TableColumn<ModelTableRow, ?> tc) {
if(isColumnEnabled(tc)) {
table.getColumns().add(tc);
}
}
private void filter(String filter) {
lock.lock();
try {
if (StringUtil.isBlank(filter)) {
observableModels.addAll(filteredModels);
filteredModels.clear();
return;
}
String[] tokens = filter.split(" ");
observableModels.addAll(filteredModels);
filteredModels.clear();
for (int i = 0; i < table.getItems().size(); i++) {
StringBuilder sb = new StringBuilder();
for (TableColumn<ModelTableRow, ?> tc : table.getColumns()) {
String cellData = tc.getCellData(i).toString();
sb.append(cellData).append(' ');
}
String searchText = sb.toString();
boolean tokensMissing = false;
for (String token : tokens) {
if(!searchText.toLowerCase().contains(token.toLowerCase())) {
tokensMissing = true;
break;
}
}
if(tokensMissing) {
ModelTableRow sessionState = table.getItems().get(i);
filteredModels.add(sessionState);
}
}
observableModels.removeAll(filteredModels);
} finally {
lock.unlock();
int filtered = filteredModels.size();
int showing = observableModels.size();
int total = showing + filtered;
count.setText(showing + "/" + total);
}
}
private void showColumnSelection(ActionEvent evt) {
ContextMenu menu = new ContextMenu();
for (TableColumn<ModelTableRow, ?> tc : columns) {
CheckMenuItem item = new CheckMenuItem(tc.getText());
item.setSelected(isColumnEnabled(tc));
menu.getItems().add(item);
item.setOnAction(e -> {
if(item.isSelected()) {
Config.getInstance().getSettings().mfcDisabledModelsTableColumns.remove(tc.getText());
for (int i = table.getColumns().size()-1; i>=0; i--) {
TableColumn<ModelTableRow, ?> other = table.getColumns().get(i);
int idx = (int) tc.getUserData();
int otherIdx = (int) other.getUserData();
if(otherIdx < idx) {
table.getColumns().add(i+1, tc);
break;
}
}
} else {
Config.getInstance().getSettings().mfcDisabledModelsTableColumns.add(tc.getText());
table.getColumns().remove(tc);
}
});
}
Button src = (Button) evt.getSource();
Point2D location = src.localToScreen(src.getTranslateX(), src.getTranslateY());
menu.show(getTabPane().getScene().getWindow(), location.getX(), location.getY() + src.getHeight() + 5);
}
private boolean isColumnEnabled(TableColumn<ModelTableRow, ?> tc) {
return !Config.getInstance().getSettings().mfcDisabledModelsTableColumns.contains(tc.getText());
}
private <T> TableColumn<ModelTableRow, T> createTableColumn(String text, int width, int idx) {
TableColumn<ModelTableRow, T> tc = new TableColumn<>(text);
tc.setPrefWidth(width);
tc.sortTypeProperty().addListener((obs, o, n) -> saveState());
tc.widthProperty().addListener((obs, o, n) -> saveState());
tc.setUserData(idx);
columns.add(tc);
return tc;
}
@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();
}
}
private void saveState() {
if (!table.getSortOrder().isEmpty()) {
TableColumn<ModelTableRow, ?> col = table.getSortOrder().get(0);
Config.getInstance().getSettings().mfcModelsTableSortColumn = col.getText();
Config.getInstance().getSettings().mfcModelsTableSortType = col.getSortType().toString();
}
double[] columnWidths = new double[table.getColumns().size()];
for (int i = 0; i < columnWidths.length; i++) {
columnWidths[i] = table.getColumns().get(i).getWidth();
}
Config.getInstance().getSettings().mfcModelsTableColumnWidths = columnWidths;
};
private void restoreState() {
String sortCol = Config.getInstance().getSettings().mfcModelsTableSortColumn;
if (StringUtil.isNotBlank(sortCol)) {
for (TableColumn<ModelTableRow, ?> col : table.getColumns()) {
if (Objects.equals(sortCol, col.getText())) {
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().mfcModelsTableSortType));
table.getSortOrder().clear();
table.getSortOrder().add(col);
break;
}
}
}
double[] columnWidths = Config.getInstance().getSettings().mfcModelsTableColumnWidths;
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
for (int i = 0; i < columnWidths.length; i++) {
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
}
}
filterInput.setText(Config.getInstance().getSettings().mfcModelsTableFilter);
}
private ListChangeListener<TableColumn<ModelTableRow, ?>> createSortOrderChangedListener() {
return new ListChangeListener<TableColumn<ModelTableRow, ?>>() {
@Override
public void onChanged(Change<? extends TableColumn<ModelTableRow, ?>> c) {
saveState();
}
};
}
private static class ModelTableRow {
private Integer uid;
private StringProperty name = new SimpleStringProperty();
private StringProperty state = new SimpleStringProperty();
private DoubleProperty camScore = new SimpleDoubleProperty();
private StringProperty newModel = new SimpleStringProperty();
private StringProperty ethnic = new SimpleStringProperty();
private StringProperty country = new SimpleStringProperty();
private StringProperty continent = new SimpleStringProperty();
private StringProperty occupation = new SimpleStringProperty();
private StringProperty tags = new SimpleStringProperty();
private StringProperty blurp = new SimpleStringProperty();
private StringProperty topic = new SimpleStringProperty();
public ModelTableRow(SessionState st) {
update(st);
}
public void update(SessionState st) {
uid = st.getUid();
name.set(Optional.ofNullable(st.getNm()).orElse("n/a"));
state.set(Optional.ofNullable(st.getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString()).orElse("n/a"));
camScore.set(Optional.ofNullable(st.getM()).map(m -> m.getCamscore()).orElse(0d));
Integer nu = Optional.ofNullable(st.getM()).map(m -> m.getNewModel()).orElse(0);
newModel.set(nu == 1 ? "new" : "");
ethnic.set(Optional.ofNullable(st.getU()).map(u -> u.getEthnic()).orElse("n/a"));
country.set(Optional.ofNullable(st.getU()).map(u -> u.getCountry()).orElse("n/a"));
continent.set(Optional.ofNullable(st.getM()).map(m -> m.getContinent()).orElse("n/a"));
occupation.set(Optional.ofNullable(st.getU()).map(u -> u.getOccupation()).orElse("n/a"));
Set<String> tagSet = Optional.ofNullable(st.getM()).map(m -> m.getTags()).orElse(Collections.emptySet());
if(tagSet.isEmpty()) {
tags.set("");
} else {
StringBuilder sb = new StringBuilder();
for (String t : tagSet) {
sb.append(t).append(',').append(' ');
}
tags.set(sb.substring(0, sb.length()-2));
}
blurp.set(Optional.ofNullable(st.getU()).map(u -> u.getBlurb()).orElse("n/a"));
String tpc = Optional.ofNullable(st.getM()).map(m -> m.getTopic()).orElse("n/a");
try {
tpc = URLDecoder.decode(tpc, "utf-8");
} catch (UnsupportedEncodingException e) {
LOG.warn("Couldn't url decode topic", e);
}
topic.set(tpc);
}
public StringProperty nameProperty() {
return name;
};
public StringProperty stateProperty() {
return state;
};
public DoubleProperty camScoreProperty() {
return camScore;
};
public StringProperty newModelProperty() {
return newModel;
};
public StringProperty ethnicityProperty() {
return ethnic;
};
public StringProperty countryProperty() {
return country;
};
public StringProperty continentProperty() {
return continent;
};
public StringProperty occupationProperty() {
return occupation;
};
public StringProperty tagsProperty() {
return tags;
};
public StringProperty blurpProperty() {
return blurp;
};
public StringProperty topicProperty() {
return topic;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((uid == null) ? 0 : uid.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;
ModelTableRow other = (ModelTableRow) obj;
if (uid == null) {
if (other.uid != null)
return false;
} else if (!uid.equals(other.uid))
return false;
return true;
};
}
}

View File

@ -0,0 +1,31 @@
package ctbrec.ui.sites.myfreecams;
import java.io.IOException;
import java.util.Collection;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.mfc.MyFreeCamsClient;
import ctbrec.sites.mfc.SessionState;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
public class TableUpdateService extends ScheduledService<Collection<SessionState>> {
private MyFreeCams mfc;
public TableUpdateService(MyFreeCams mfc) {
this.mfc = mfc;
}
@Override
protected Task<Collection<SessionState>> createTask() {
return new Task<Collection<SessionState>>() {
@Override
public Collection<SessionState> call() throws IOException {
MyFreeCamsClient client = mfc.getClient();
return client.getSessionStates();
}
};
}
}

View File

@ -0,0 +1,86 @@
package ctbrec.ui.sites.streamate;
import ctbrec.Config;
import ctbrec.Settings;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class StreamateConfigUI extends AbstractConfigUI {
private Streamate streamate;
public StreamateConfigUI(Streamate streamate) {
this.streamate = streamate;
}
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
Settings settings = Config.getInstance().getSettings();
int row = 0;
Label l = new Label("Active");
layout.add(l, 0, row);
CheckBox enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(streamate.getName()));
enabled.setOnAction((e) -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(streamate.getName());
} else {
settings.disabledSites.add(streamate.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("Streamate User"), 0, row);
TextField username = new TextField(settings.streamateUsername);
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().streamateUsername)) {
Config.getInstance().getSettings().streamateUsername = username.getText();
streamate.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, row++);
layout.add(new Label("Streamate Password"), 0, row);
PasswordField password = new PasswordField();
password.setText(settings.streamatePassword);
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().streamatePassword)) {
Config.getInstance().getSettings().streamatePassword = password.getText();
streamate.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, row++);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntegration.open(streamate.getAffiliateLink()));
layout.add(createAccount, 1, row++);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
return layout;
}
}

View File

@ -0,0 +1,93 @@
package ctbrec.ui.sites.streamate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.streamate.StreamateHttpClient;
import ctbrec.sites.streamate.StreamateModel;
import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
import okhttp3.Response;
public class StreamateFollowedService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateFollowedService.class);
private static final int MODELS_PER_PAGE = 48;
private Streamate streamate;
private StreamateHttpClient httpClient;
private String url;
private boolean showOnline = true;
public StreamateFollowedService(Streamate streamate) {
this.streamate = streamate;
this.httpClient = (StreamateHttpClient) streamate.getHttpClient();
this.url = streamate.getBaseUrl() + "/api/search/v1/favorites?host=streamate.com&domain=streamate.com";
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException {
httpClient.login();
String saKey = httpClient.getSaKey();
String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey;
LOG.debug("Fetching page {}", _url);
Request request = new Request.Builder()
.url(_url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", streamate.getBaseUrl())
.build();
try(Response response = streamate.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
List<Model> models = new ArrayList<>();
String content = response.body().string();
JSONObject json = new JSONObject(content);
if(json.optString("status").equals("SM_OK")) {
JSONArray performers = json.getJSONArray("Results");
for (int i = 0; i < performers.length(); i++) {
JSONObject p = performers.getJSONObject(i);
String nickname = p.getString("Nickname");
StreamateModel model = (StreamateModel) streamate.createModel(nickname);
model.setId(p.getLong("PerformerId"));
model.setPreview("https://m1.nsimg.net/biopic/320x240/" + model.getId());
boolean online = p.optString("LiveStatus").equals("live");
model.setOnline(online);
if(online == showOnline) {
models.add(model);
}
}
} else {
throw new IOException("Status: " + json.optString("status"));
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
};
}
public void setOnline(boolean online) {
this.showOnline = online;
}
}

View File

@ -0,0 +1,77 @@
package ctbrec.ui.sites.streamate;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.FollowedTab;
import ctbrec.ui.ThumbOverviewTab;
import javafx.concurrent.WorkerStateEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
public class StreamateFollowedTab extends ThumbOverviewTab implements FollowedTab {
private Label status;
public StreamateFollowedTab(Streamate streamate) {
super("Favorites", new StreamateFollowedService(streamate), streamate);
status = new Label("Logging in...");
grid.getChildren().add(status);
}
@Override
protected void createGui() {
super.createGui();
addOnlineOfflineSelector();
}
private void addOnlineOfflineSelector() {
ToggleGroup group = new ToggleGroup();
RadioButton online = new RadioButton("online");
online.setToggleGroup(group);
RadioButton offline = new RadioButton("offline");
offline.setToggleGroup(group);
pagination.getChildren().add(online);
pagination.getChildren().add(offline);
HBox.setMargin(online, new Insets(5,5,5,40));
HBox.setMargin(offline, new Insets(5,5,5,5));
online.setSelected(true);
group.selectedToggleProperty().addListener((e) -> {
((StreamateFollowedService)updateService).setOnline(online.isSelected());
queue.clear();
updateService.restart();
});
}
@Override
protected void onSuccess() {
grid.getChildren().remove(status);
super.onSuccess();
}
@Override
protected void onFail(WorkerStateEvent event) {
status.setText("Login failed");
super.onFail(event);
}
@Override
public void selected() {
status.setText("Logging in...");
super.selected();
}
public void setScene(Scene scene) {
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if(this.isSelected()) {
if(event.getCode() == KeyCode.DELETE) {
follow(selectedThumbCells, false);
}
}
});
}
}

View File

@ -0,0 +1,37 @@
package ctbrec.ui.sites.streamate;
import java.io.IOException;
import ctbrec.sites.ConfigUI;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider;
public class StreamateSiteUi implements SiteUI {
private StreamateTabProvider tabProvider;
private StreamateConfigUI configUi;
private Streamate streamate;
public StreamateSiteUi(Streamate streamate) {
this.streamate = streamate;
tabProvider = new StreamateTabProvider(streamate);
configUi = new StreamateConfigUI(streamate);
}
@Override
public TabProvider getTabProvider() {
return tabProvider;
}
@Override
public ConfigUI getConfigUI() {
return configUi;
}
@Override
public boolean login() throws IOException {
return streamate.login();
}
}

View File

@ -0,0 +1,62 @@
package ctbrec.ui.sites.streamate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.recorder.Recorder;
import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.TabProvider;
import ctbrec.ui.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
public class StreamateTabProvider extends TabProvider {
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateTabProvider.class);
private Streamate streamate;
private Recorder recorder;
private ThumbOverviewTab followedTab;
public StreamateTabProvider(Streamate streamate) {
this.streamate = streamate;
this.recorder = streamate.getRecorder();
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
try {
tabs.add(createTab("Girls", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:f"));
tabs.add(createTab("Guys", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:m"));
tabs.add(createTab("Couples", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:mf"));
tabs.add(createTab("Lesbian", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:ff"));
tabs.add(createTab("Gay", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:mm"));
tabs.add(createTab("Groups", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:g"));
tabs.add(createTab("Trans female", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tm2f"));
tabs.add(createTab("Trans male", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tf2m"));
tabs.add(createTab("New", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=new:true"));
followedTab = new StreamateFollowedTab(streamate);
followedTab.setRecorder(recorder);
tabs.add(followedTab);
} catch (IOException e) {
LOG.error("Couldn't create streamate tab", e);
}
return tabs;
}
@Override
public Tab getFollowedTab() {
return followedTab;
}
private Tab createTab(String title, String url) throws IOException {
StreamateUpdateService updateService = new StreamateUpdateService(streamate, url);
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, streamate);
tab.setRecorder(recorder);
return tab;
}
}

View File

@ -0,0 +1,86 @@
package ctbrec.ui.sites.streamate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.streamate.StreamateModel;
import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
import okhttp3.Response;
public class StreamateUpdateService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateUpdateService.class);
private static final int MODELS_PER_PAGE = 48;
private Streamate streamate;
private String url;
public StreamateUpdateService(Streamate streamate, String url) {
this.streamate = streamate;
this.url = url;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException {
int from = (page - 1) * MODELS_PER_PAGE;
String _url = url + "&from=" + from + "&size=" + MODELS_PER_PAGE;
LOG.debug("Fetching page {}", _url);
Request request = new Request.Builder()
.url(_url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", streamate.getBaseUrl())
.build();
try(Response response = streamate.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
List<Model> models = new ArrayList<>();
String content = response.body().string();
JSONObject json = new JSONObject(content);
JSONArray performers = json.getJSONArray("performers");
for (int i = 0; i < performers.length(); i++) {
JSONObject p = performers.getJSONObject(i);
String nickname = p.getString("nickname");
StreamateModel model = (StreamateModel) streamate.createModel(nickname);
model.setId(p.getLong("id"));
model.setPreview(p.getString("thumbnail"));
model.setOnline(p.optBoolean("online"));
// TODO figure out, what all the states mean
// liveState {}
// exclusiveShow false
// goldShow true
// onBreak false
// partyChat true
// preGoldShow true
// privateChat false
// specialShow false
models.add(model);
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
};
}
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,7 @@
.root {
-fx-base: #4d4d4d;
-fx-accent: #0096c9;
-fx-default-button: -fx-accent;
-fx-focus-color: -fx-accent;
-fx-control-inner-background-alt: derive(-fx-base, 95%);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -8,7 +8,7 @@
<parent>
<groupId>ctbrec</groupId>
<artifactId>master</artifactId>
<version>1.10.0</version>
<version>1.15.0</version>
<relativePath>../master</relativePath>
</parent>

View File

@ -3,6 +3,7 @@ package ctbrec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import com.squareup.moshi.JsonReader;
@ -14,12 +15,14 @@ public abstract class AbstractModel implements Model {
private String url;
private String name;
private String displayName;
private String preview;
private String description;
private List<String> tags = new ArrayList<>();
private int streamUrlIndex = -1;
private boolean suspended = false;
protected Site site;
protected State onlineState = State.UNKNOWN;
@Override
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
@ -46,6 +49,20 @@ public abstract class AbstractModel implements Model {
this.name = name;
}
@Override
public String getDisplayName() {
if(displayName != null) {
return displayName;
} else {
return getName();
}
}
@Override
public void setDisplayName(String name) {
this.displayName = name;
}
@Override
public String getPreview() {
return preview;
@ -106,6 +123,15 @@ public abstract class AbstractModel implements Model {
this.suspended = suspended;
}
@Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
return onlineState;
}
public void setOnlineState(State status) {
this.onlineState = status;
}
@Override
public int hashCode() {
final int prime = 31;
@ -137,6 +163,13 @@ public abstract class AbstractModel implements Model {
return true;
}
@Override
public int compareTo(Model o) {
String thisName = Optional.ofNullable(getDisplayName()).orElse("").toLowerCase();
String otherName = Optional.ofNullable(o).map(m -> m.getDisplayName()).orElse("").toLowerCase();
return thisName.compareTo(otherName);
}
@Override
public String toString() {
return getName();

View File

@ -7,6 +7,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@ -46,7 +47,6 @@ public class Config {
} else {
filename = "settings.json";
}
load();
}
private void load() throws FileNotFoundException, IOException {
@ -61,6 +61,13 @@ public class Config {
BufferedSource source = buffer.readFrom(fin);
settings = adapter.fromJson(source);
settings.httpTimeout = Math.max(settings.httpTimeout, 10_000);
} catch(Throwable e) {
settings = OS.getDefaultSettings();
for (Site site : sites) {
site.setEnabled(!settings.disabledSites.contains(site.getName()));
}
makeBackup(configFile);
throw e;
}
} else {
LOG.error("Config file does not exist. Falling back to default values.");
@ -71,9 +78,22 @@ public class Config {
}
}
private void makeBackup(File source) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
String timestamp = sdf.format(new Date());
String backup = source.getName() + '.' + timestamp;
File target = new File(source.getParentFile(), backup);
Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch(Throwable e) {
LOG.error("Couldn't create backup of settings file", e);
}
}
public static synchronized void init(List<Site> sites) throws FileNotFoundException, IOException {
if(instance == null) {
instance = new Config(sites);
instance.load();
}
}
@ -100,10 +120,14 @@ public class Config {
Files.write(configFile.toPath(), json.getBytes("utf-8"), CREATE, WRITE, TRUNCATE_EXISTING);
}
public boolean isServerMode() {
public static boolean isServerMode() {
return Objects.equals(System.getProperty("ctbrec.server.mode"), "1");
}
public static boolean isDevMode() {
return Objects.equals(System.getenv("CTBREC_DEV"), "1");
}
public File getConfigDir() {
return configDir;
}
@ -113,10 +137,6 @@ public class Config {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String startTime = sdf.format(new Date());
File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts");
if(getSettings().splitRecordings > 0) {
LOG.debug("Splitting recordings every {} seconds", getSettings().splitRecordings);
targetFile = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
}
return targetFile;
}

View File

@ -12,33 +12,95 @@ import com.squareup.moshi.JsonWriter;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
public interface Model {
public interface Model extends Comparable<Model> {
public static enum State {
ONLINE("online"),
OFFLINE("offline"),
AWAY("away"),
PRIVATE("private"),
GROUP("group"),
UNKNOWN("unknown");
String display;
State(String display) {
this.display = display;
}
@Override
public String toString() {
return display;
}
}
public String getUrl();
public void setUrl(String url);
public String getDisplayName();
public void setDisplayName(String name);
public String getName();
public void setName(String name);
public String getPreview();
public void setPreview(String preview);
public List<String> getTags();
public void setTags(List<String> tags);
public String getDescription();
public void setDescription(String description);
public int getStreamUrlIndex();
public void setStreamUrlIndex(int streamUrlIndex);
public boolean isOnline() throws IOException, ExecutionException, InterruptedException;
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException;
public String getOnlineState(boolean failFast) throws IOException, ExecutionException;
public State getOnlineState(boolean failFast) throws IOException, ExecutionException;
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
public void invalidateCacheEntries();
public void receiveTip(int tokens) throws IOException;
/**
* Determines the stream resolution for this model
*
* @param failFast
* If set to true, the method returns emmediately, even if the resolution is unknown. If
* the resolution is unknown, the array contains 0,0
*
* @return a tupel of width and height represented by an int[2]
* @throws ExecutionException
*/
public int[] getStreamResolution(boolean failFast) throws ExecutionException;
public boolean follow() throws IOException;
public boolean unfollow() throws IOException;
public void setSite(Site site);
public Site getSite();
public void writeSiteSpecificData(JsonWriter writer) throws IOException;
public void readSiteSpecificData(JsonReader reader) throws IOException;
public boolean isSuspended();
public void setSuspended(boolean suspended);
}

View File

@ -0,0 +1,73 @@
package ctbrec;
import java.io.File;
import java.io.IOException;
import java.nio.channels.ReadableByteChannel;
import java.time.Duration;
import java.util.Set;
import org.jcodec.common.Demuxer;
import org.jcodec.common.DemuxerTrack;
import org.jcodec.common.TrackType;
import org.jcodec.common.Tuple;
import org.jcodec.common.Tuple._2;
import org.jcodec.common.io.FileChannelWrapper;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.model.Packet;
import org.jcodec.containers.mps.MPSDemuxer;
import org.jcodec.containers.mps.MTSDemuxer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MpegUtil {
private static final transient Logger LOG = LoggerFactory.getLogger(MpegUtil.class);
public static void main(String[] args) throws IOException {
readFile(new File("../../test-recs/ff.ts"));
}
public static void readFile(File file) throws IOException {
System.out.println(file.getCanonicalPath());
double duration = MpegUtil.getFileDuration(file);
System.out.println(Duration.ofSeconds((long) duration));
}
public static double getFileDuration(File file) throws IOException {
try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) {
_2<Integer,Demuxer> m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO);
Demuxer demuxer = m2tsDemuxer.v1;
DemuxerTrack videoDemux = demuxer.getTracks().get(0);
Packet videoFrame = null;
double totalDuration = 0;
while( (videoFrame = videoDemux.nextFrame()) != null) {
totalDuration += videoFrame.getDurationD();
}
return totalDuration;
}
}
public static _2<Integer, Demuxer> createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException {
MTSDemuxer mts = new MTSDemuxer(ch);
Set<Integer> programs = mts.getPrograms();
if (programs.size() == 0) {
LOG.error("The MPEG TS stream contains no programs");
return null;
}
Tuple._2<Integer, Demuxer> found = null;
for (Integer pid : programs) {
ReadableByteChannel program = mts.getProgram(pid);
if (found != null) {
program.close();
continue;
}
MPSDemuxer demuxer = new MPSDemuxer(program);
if (targetTrack == TrackType.AUDIO && demuxer.getAudioTracks().size() > 0
|| targetTrack == TrackType.VIDEO && demuxer.getVideoTracks().size() > 0) {
found = org.jcodec.common.Tuple._2(pid, (Demuxer) demuxer);
} else {
program.close();
}
}
return found;
}
}

View File

@ -1,12 +1,26 @@
package ctbrec;
import java.awt.AWTException;
import java.awt.Image;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.StreamRedirectThread;
public class OS {
private static final transient Logger LOG = LoggerFactory.getLogger(OS.class);
public static enum TYPE {
LINUX,
MAC,
@ -72,4 +86,61 @@ public class OS {
}
return env;
}
public static void notification(String title, String header, String msg) {
if(OS.getOsType() == OS.TYPE.LINUX) {
notifyLinux(title, header, msg);
} else if(OS.getOsType() == OS.TYPE.WINDOWS) {
notifyWindows(title, header, msg);
} else if(OS.getOsType() == OS.TYPE.MAC) {
notifyMac(title, header, msg);
} else {
// unknown system, try systemtray notification anyways
notifySystemTray(title, header, msg);
}
}
private static void notifyLinux(String title, String header, String msg) {
try {
Process p = Runtime.getRuntime().exec(new String[] {
"notify-send",
"-u", "normal",
"-t", "5000",
"-a", title,
header,
msg.replaceAll("-", "\\\\-").replaceAll("\\s", "\\\\ "),
"--icon=dialog-information"
});
new Thread(new StreamRedirectThread(p.getInputStream(), System.out)).start();
new Thread(new StreamRedirectThread(p.getErrorStream(), System.err)).start();
} catch (IOException e1) {
LOG.error("Notification failed", e1);
}
}
private static void notifyWindows(String title, String header, String msg) {
notifySystemTray(title, header, msg);
}
private static void notifyMac(String title, String header, String msg) {
notifySystemTray(title, header, msg);
}
private static void notifySystemTray(String title, String header, String msg) {
if(SystemTray.isSupported()) {
SystemTray tray = SystemTray.getSystemTray();
Image image = Toolkit.getDefaultToolkit().createImage(OS.class.getResource("/icon64.png"));
TrayIcon trayIcon = new TrayIcon(image, title);
trayIcon.setImageAutoSize(true);
trayIcon.setToolTip(title);
try {
tray.add(trayIcon);
} catch (AWTException e) {
LOG.error("Coulnd't add tray icon", e);
}
trayIcon.displayMessage(header, msg, MessageType.INFO);
} else {
LOG.error("SystemTray notifications not supported by this OS");
}
}
}

View File

@ -10,16 +10,29 @@ public class Recording {
private Instant startDate;
private String path;
private boolean hasPlaylist;
private STATUS status;
private State status = State.UNKNOWN;
private int progress = -1;
private long sizeInByte;
public static enum STATUS {
RECORDING,
GENERATING_PLAYLIST,
FINISHED,
DOWNLOADING,
MERGING
public static enum State {
RECORDING("recording"),
STOPPED("stopped"),
GENERATING_PLAYLIST("generating playlist"),
POST_PROCESSING("post-processing"),
FINISHED("finished"),
DOWNLOADING("downloading"),
UNKNOWN("unknown");
private String desc;
State(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return desc;
}
}
public Recording() {}
@ -48,11 +61,11 @@ public class Recording {
this.startDate = startDate;
}
public STATUS getStatus() {
public State getStatus() {
return status;
}
public void setStatus(STATUS status) {
public void setStatus(State status) {
this.status = status;
}

View File

@ -4,6 +4,8 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import ctbrec.event.EventHandlerConfiguration;
public class Settings {
public enum ProxyType {
@ -30,6 +32,7 @@ public class Settings {
}
public boolean singlePlayer = true;
public boolean showPlayerStarting = false;
public boolean localRecording = true;
public int httpPort = 8080;
public int httpTimeout = 10000;
@ -37,22 +40,36 @@ public class Settings {
public String httpServer = "localhost";
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
public long minimumSpaceLeftInBytes = 0;
public int minimumLengthInSeconds = 0;
public String mediaPlayer = "/usr/bin/mpv";
public String postProcessing = "";
public String username = ""; // chaturbate username TODO maybe rename this onetime
public String password = ""; // chaturbate password TODO maybe rename this onetime
public String chaturbateBaseUrl = "https://chaturbate.com";
public String bongaUsername = "";
public String bongaPassword = "";
public String mfcUsername = "";
public String mfcPassword = "";
public String mfcBaseUrl = "https://www.myfreecams.com";
public String mfcModelsTableSortColumn = "";
public String mfcModelsTableSortType = "";
public double[] mfcModelsTableColumnWidths = new double[0];
public String mfcModelsTableFilter = "";
public List<String> mfcDisabledModelsTableColumns = new ArrayList<>();
public boolean mfcIgnoreUpscaled = false;
public String camsodaUsername = "";
public String camsodaPassword = "";
public String cam4Username;
public String cam4Password;
public String cam4Username = "";
public String cam4Password = "";
public String streamateUsername = "";
public String streamatePassword = "";
public String lastDownloadDir = "";
public List<Model> models = new ArrayList<Model>();
public List<Model> models = new ArrayList<>();
public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>();
public boolean determineResolution = false;
public boolean previewInThumbnails = true;
public boolean requireAuthentication = false;
public boolean chooseStreamQuality = false;
public int maximumResolution = 0;
@ -62,7 +79,9 @@ public class Settings {
public String proxyPort;
public String proxyUser;
public String proxyPassword;
public String startTab = "Settings";
public int thumbWidth = 180;
public boolean updateThumbnails = true;
public int windowWidth = 1340;
public int windowHeight = 800;
public boolean windowMaximized = false;
@ -70,4 +89,13 @@ public class Settings {
public int windowY;
public int splitRecordings = 0;
public List<String> disabledSites = new ArrayList<>();
public String colorBase = "#FFFFFF";
public String colorAccent = "#FFFFFF";
public int onlineCheckIntervalInSecs = 60;
public String recordedModelsSortColumn = "";
public String recordedModelsSortType = "";
public double[] recordedModelsColumnWidths = new double[0];
public String recordingsSortColumn = "";
public String recordingsSortType = "";
public double[] recordingsColumnWidths = new double[0];
}

View File

@ -1,5 +1,7 @@
package ctbrec;
import java.text.DecimalFormat;
public class StringUtil {
public static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
@ -8,4 +10,21 @@ public class StringUtil {
public static boolean isNotBlank(String s) {
return !isBlank(s);
}
public static String formatSize(Number sizeInByte) {
DecimalFormat df = new DecimalFormat("0.00");
String unit = "Bytes";
double size = sizeInByte.doubleValue();
if(size > 1024.0 * 1024 * 1024) {
size = size / 1024.0 / 1024 / 1024;
unit = "GiB";
} else if(size > 1024.0 * 1024) {
size = size / 1024.0 / 1024;
unit = "MiB";
} else if(size > 1024.0) {
size = size / 1024.0;
unit = "KiB";
}
return df.format(size) + ' ' + unit;
}
}

View File

@ -0,0 +1,16 @@
package ctbrec.event;
import ctbrec.Model;
public abstract class AbstractModelEvent extends Event {
protected Model model;
public Model getModel() {
return model;
}
public void setModel(Model model) {
this.model = model;
}
}

View File

@ -0,0 +1,25 @@
package ctbrec.event;
import java.util.function.Consumer;
import ctbrec.event.EventHandlerConfiguration.ActionConfiguration;
public abstract class Action implements Consumer<Event> {
protected String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void configure(ActionConfiguration config) throws Exception;
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,39 @@
package ctbrec.event;
public abstract class Event {
public static enum Type {
/**
* This event is fired every time the OnlineMonitor sees a model online
* It is also fired, if the model was online before. You can see it as a "still online ping".
*/
MODEL_ONLINE("model is online"),
/**
* This event is fired whenever the model's online state (Model.STATUS) changes.
*/
MODEL_STATUS_CHANGED("model status changed"),
/**
* This event is fired whenever the state of a recording changes.
*/
RECORDING_STATUS_CHANGED("recording status changed");
private String desc;
Type(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return desc;
}
}
public abstract Type getType();
public abstract String getName();
public abstract String getDescription();
public abstract String[] getExecutionParams();
}

Some files were not shown because too many files have changed in this diff Show More