Merge branch 'dev' into fc2
# Conflicts: # common/src/main/java/ctbrec/io/HttpClient.java
This commit is contained in:
commit
a0a083aaf6
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: Other
|
||||
about: Anything else
|
||||
|
||||
---
|
||||
|
||||
|
75
CHANGELOG.md
75
CHANGELOG.md
|
@ -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,7 +2,7 @@
|
|||
/target/
|
||||
*~
|
||||
*.bak
|
||||
/ctbrec.log
|
||||
/*.log
|
||||
/ctbrec-tunnel.sh
|
||||
/jre/
|
||||
/server-local.sh
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
pushd $(dirname $0)
|
||||
JAVA=./jre/bin/java
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
pushd $(dirname $0)
|
||||
JAVA=java
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,42 +104,74 @@ 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();
|
||||
if(popup != null) {
|
||||
if (popup != null) {
|
||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
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) {
|
||||
if (popup != null) {
|
||||
popup.hide();
|
||||
}
|
||||
});
|
||||
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if(event.getCode() == KeyCode.DELETE) {
|
||||
stopAction();
|
||||
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
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,15 +273,22 @@ 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();
|
||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||
updateService.setOnSucceeded((event) -> {
|
||||
List<JavaFxModel> models = updateService.getValue();
|
||||
if(models == null) {
|
||||
if (models == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -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,33 +411,47 @@ 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) {
|
||||
try {
|
||||
if(!fxModel.isOnline()) {
|
||||
if (!fxModel.isOnline()) {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle("Switch resolution");
|
||||
alert.setHeaderText("Couldn't switch stream resolution");
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
if (recording != null) {
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
delete(recording);
|
||||
} else if (event.getCode() == KeyCode.ENTER) {
|
||||
if(recording.getStatus() == STATUS.FINISHED) {
|
||||
table.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
Recording recording = table.getSelectionModel().getSelectedItem();
|
||||
if(recording != null) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"))) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ctbrec.ui.autofilltextbox;
|
||||
package ctbrec.ui.controls;
|
||||
|
||||
|
||||
import javafx.collections.ObservableList;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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... >" 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2008, 2014, Oracle and/or its affiliates.
|
||||
* All rights reserved. Use is subject to license terms.
|
||||
*
|
||||
* This file is available and licensed under the following license:
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the distribution.
|
||||
* - Neither the name of Oracle Corporation nor the names of its
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package ctbrec.ui.controls;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
/**
|
||||
* Search field with styling and a clear button
|
||||
*/
|
||||
public class SearchBox extends TextField implements ChangeListener<String>{
|
||||
private final Button clearButton = new Button();
|
||||
private final Region innerBackground = new Region();
|
||||
private final Region icon = new Region();
|
||||
private final int prefHeight = 26;
|
||||
|
||||
public SearchBox() {
|
||||
getStyleClass().addAll("search-box");
|
||||
icon.getStyleClass().setAll("search-box-icon");
|
||||
innerBackground.getStyleClass().setAll("search-box-inner");
|
||||
setPromptText("Search");
|
||||
textProperty().addListener(this);
|
||||
setPrefHeight(prefHeight);
|
||||
clearButton.getStyleClass().setAll("search-clear-button");
|
||||
clearButton.setCursor(Cursor.DEFAULT);
|
||||
clearButton.setOnMouseClicked((MouseEvent t) -> {
|
||||
setText("");
|
||||
requestFocus();
|
||||
});
|
||||
clearButton.setVisible(false);
|
||||
clearButton.setManaged(false);
|
||||
innerBackground.setManaged(false);
|
||||
icon.setManaged(false);
|
||||
}
|
||||
|
||||
public SearchBox(boolean icon) {
|
||||
this();
|
||||
this.icon.setVisible(false);
|
||||
this.icon.getStyleClass().remove("search-box-icon");
|
||||
this.setStyle("-fx-padding: 5");
|
||||
}
|
||||
|
||||
@Override protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
if (clearButton.getParent() != this) getChildren().add(clearButton);
|
||||
if (innerBackground.getParent() != this) getChildren().add(0,innerBackground);
|
||||
if (icon.getParent() != this) getChildren().add(icon);
|
||||
innerBackground.setLayoutX(0);
|
||||
innerBackground.setLayoutY(0);
|
||||
innerBackground.resize(getWidth(), getHeight());
|
||||
icon.setLayoutX(0);
|
||||
icon.setLayoutY(0);
|
||||
icon.resize(35,prefHeight);
|
||||
clearButton.setLayoutX(getWidth() - prefHeight);
|
||||
clearButton.setLayoutY(0);
|
||||
clearButton.resize(prefHeight, prefHeight);
|
||||
}
|
||||
|
||||
@Override public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) {
|
||||
clearButton.setVisible(newValue.length() > 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package ctbrec.ui.controls;
|
||||
|
||||
public class SearchPopover extends Popover {
|
||||
|
||||
|
||||
public SearchPopover() {
|
||||
getStyleClass().add("right-tooth");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
*/
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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() {
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ public class MyFreeCamsSiteUi implements SiteUI {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean login() throws IOException {
|
||||
public synchronized boolean login() throws IOException {
|
||||
return myFreeCams.login();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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 |
|
@ -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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue