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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
77
CHANGELOG.md
77
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
|
1.10.0
|
||||||
========================
|
========================
|
||||||
* Fix: HMAC authentication didn't work for playing and downloading of a
|
* Fix: HMAC authentication didn't work for playing and downloading of a
|
||||||
|
@ -113,4 +188,4 @@
|
||||||
* Added proxy settings
|
* Added proxy settings
|
||||||
* Made playlist generator more robust
|
* Made playlist generator more robust
|
||||||
* Fixed some issues with the file merging
|
* Fixed some issues with the file merging
|
||||||
* Fixed memory leak caused by the model filter function
|
* Fixed memory leak caused by the model filter function
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/target/
|
/target/
|
||||||
*~
|
*~
|
||||||
*.bak
|
*.bak
|
||||||
/ctbrec.log
|
/*.log
|
||||||
/ctbrec-tunnel.sh
|
/ctbrec-tunnel.sh
|
||||||
/jre/
|
/jre/
|
||||||
/server-local.sh
|
/server-local.sh
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.10.0</version>
|
<version>1.15.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
@ -118,10 +118,11 @@
|
||||||
<addDependencies>false</addDependencies>
|
<addDependencies>false</addDependencies>
|
||||||
<preCp>anything</preCp>
|
<preCp>anything</preCp>
|
||||||
</classPath>
|
</classPath>
|
||||||
|
<downloadUrl>https://jdk.java.net/</downloadUrl>
|
||||||
<jre>
|
<jre>
|
||||||
<path>jre</path>
|
<path>jre</path>
|
||||||
<bundledJre64Bit>true</bundledJre64Bit>
|
<bundledJre64Bit>true</bundledJre64Bit>
|
||||||
<minVersion>1.8.0</minVersion>
|
<minVersion>10</minVersion>
|
||||||
<maxHeapSize>512</maxHeapSize>
|
<maxHeapSize>512</maxHeapSize>
|
||||||
</jre>
|
</jre>
|
||||||
<versionInfo>
|
<versionInfo>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
pushd $(dirname $0)
|
pushd $(dirname $0)
|
||||||
JAVA=./jre/bin/java
|
JAVA=./jre/bin/java
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
pushd $(dirname $0)
|
pushd $(dirname $0)
|
||||||
JAVA=java
|
JAVA=java
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.ui;
|
||||||
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
@ -9,21 +12,24 @@ import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.JsonAdapter;
|
||||||
import com.squareup.moshi.Moshi;
|
import com.squareup.moshi.Moshi;
|
||||||
import com.squareup.moshi.Types;
|
import com.squareup.moshi.Types;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
import ctbrec.Version;
|
import ctbrec.Version;
|
||||||
|
import ctbrec.event.EventBusHolder;
|
||||||
|
import ctbrec.event.EventHandler;
|
||||||
|
import ctbrec.event.EventHandlerConfiguration;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.recorder.LocalRecorder;
|
import ctbrec.recorder.LocalRecorder;
|
||||||
|
import ctbrec.recorder.OnlineMonitor;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.recorder.RemoteRecorder;
|
import ctbrec.recorder.RemoteRecorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
@ -33,6 +39,8 @@ import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.fc2live.Fc2Live;
|
import ctbrec.sites.fc2live.Fc2Live;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.HostServices;
|
import javafx.application.HostServices;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
@ -43,6 +51,7 @@ import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TabPane;
|
import javafx.scene.control.TabPane;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
@ -53,12 +62,13 @@ public class CamrecApplication extends Application {
|
||||||
|
|
||||||
private Config config;
|
private Config config;
|
||||||
private Recorder recorder;
|
private Recorder recorder;
|
||||||
|
private OnlineMonitor onlineMonitor;
|
||||||
static HostServices hostServices;
|
static HostServices hostServices;
|
||||||
private SettingsTab settingsTab;
|
private SettingsTab settingsTab;
|
||||||
private TabPane rootPane = new TabPane();
|
private TabPane rootPane = new TabPane();
|
||||||
static EventBus bus;
|
|
||||||
private List<Site> sites = new ArrayList<>();
|
private List<Site> sites = new ArrayList<>();
|
||||||
public static HttpClient httpClient;
|
public static HttpClient httpClient;
|
||||||
|
public static String title;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage primaryStage) throws Exception {
|
public void start(Stage primaryStage) throws Exception {
|
||||||
|
@ -69,11 +79,14 @@ public class CamrecApplication extends Application {
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new Fc2Live());
|
sites.add(new Fc2Live());
|
||||||
sites.add(new MyFreeCams());
|
sites.add(new MyFreeCams());
|
||||||
|
sites.add(new Streamate());
|
||||||
loadConfig();
|
loadConfig();
|
||||||
|
registerAlertSystem();
|
||||||
createHttpClient();
|
createHttpClient();
|
||||||
bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
|
|
||||||
hostServices = getHostServices();
|
hostServices = getHostServices();
|
||||||
createRecorder();
|
createRecorder();
|
||||||
|
onlineMonitor = new OnlineMonitor(recorder);
|
||||||
|
onlineMonitor.start();
|
||||||
for (Site site : sites) {
|
for (Site site : sites) {
|
||||||
if(site.isEnabled()) {
|
if(site.isEnabled()) {
|
||||||
try {
|
try {
|
||||||
|
@ -96,7 +109,8 @@ public class CamrecApplication extends Application {
|
||||||
|
|
||||||
private void createGui(Stage primaryStage) throws IOException {
|
private void createGui(Stage primaryStage) throws IOException {
|
||||||
LOG.debug("Creating GUI");
|
LOG.debug("Creating GUI");
|
||||||
primaryStage.setTitle("CTB Recorder " + getVersion());
|
CamrecApplication.title = "CTB Recorder " + getVersion();
|
||||||
|
primaryStage.setTitle(title);
|
||||||
InputStream icon = getClass().getResourceAsStream("/icon.png");
|
InputStream icon = getClass().getResourceAsStream("/icon.png");
|
||||||
primaryStage.getIcons().add(new Image(icon));
|
primaryStage.getIcons().add(new Image(icon));
|
||||||
int windowWidth = Config.getInstance().getSettings().windowWidth;
|
int windowWidth = Config.getInstance().getSettings().windowWidth;
|
||||||
|
@ -112,19 +126,26 @@ public class CamrecApplication extends Application {
|
||||||
rootPane.getTabs().add(siteTab);
|
rootPane.getTabs().add(siteTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
((SiteTab)rootPane.getTabs().get(0)).selected();
|
|
||||||
} catch(ClassCastException | IndexOutOfBoundsException e) {}
|
|
||||||
|
|
||||||
RecordedModelsTab modelsTab = new RecordedModelsTab("Recording", recorder, sites);
|
RecordedModelsTab modelsTab = new RecordedModelsTab("Recording", recorder, sites);
|
||||||
rootPane.getTabs().add(modelsTab);
|
rootPane.getTabs().add(modelsTab);
|
||||||
RecordingsTab recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
|
RecordingsTab recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
|
||||||
rootPane.getTabs().add(recordingsTab);
|
rootPane.getTabs().add(recordingsTab);
|
||||||
settingsTab = new SettingsTab(sites);
|
settingsTab = new SettingsTab(sites, recorder);
|
||||||
rootPane.getTabs().add(settingsTab);
|
rootPane.getTabs().add(settingsTab);
|
||||||
rootPane.getTabs().add(new DonateTabFx());
|
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/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().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue());
|
||||||
primaryStage.getScene().heightProperty()
|
primaryStage.getScene().heightProperty()
|
||||||
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
|
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
|
||||||
|
@ -146,7 +167,10 @@ public class CamrecApplication extends Application {
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
modelsTab.saveState();
|
||||||
|
recordingsTab.saveState();
|
||||||
settingsTab.saveConfig();
|
settingsTab.saveConfig();
|
||||||
|
onlineMonitor.shutdown();
|
||||||
recorder.shutdown();
|
recorder.shutdown();
|
||||||
for (Site site : sites) {
|
for (Site site : sites) {
|
||||||
if(site.isEnabled()) {
|
if(site.isEnabled()) {
|
||||||
|
@ -156,9 +180,13 @@ public class CamrecApplication extends Application {
|
||||||
try {
|
try {
|
||||||
Config.getInstance().save();
|
Config.getInstance().save();
|
||||||
LOG.info("Shutdown complete. Goodbye!");
|
LOG.info("Shutdown complete. Goodbye!");
|
||||||
Platform.exit();
|
Platform.runLater(() -> {
|
||||||
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
|
primaryStage.close();
|
||||||
System.exit(0);
|
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) {
|
} catch (IOException e1) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
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() {
|
private void createRecorder() {
|
||||||
if (config.getSettings().localRecording) {
|
if (config.getSettings().localRecording) {
|
||||||
recorder = new LocalRecorder(config);
|
recorder = new LocalRecorder(config);
|
||||||
|
@ -201,9 +287,8 @@ public class CamrecApplication extends Application {
|
||||||
LOG.error("Couldn't load settings", e);
|
LOG.error("Couldn't load settings", e);
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||||
alert.setTitle("Whoopsie");
|
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();
|
alert.showAndWait();
|
||||||
System.exit(1);
|
|
||||||
}
|
}
|
||||||
config = Config.getInstance();
|
config = Config.getInstance();
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,9 @@ import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.Background;
|
|
||||||
import javafx.scene.layout.BackgroundFill;
|
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.CornerRadii;
|
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.scene.text.TextAlignment;
|
import javafx.scene.text.TextAlignment;
|
||||||
|
|
||||||
|
@ -26,7 +22,6 @@ public class DonateTabFx extends Tab {
|
||||||
setText("Donate");
|
setText("Donate");
|
||||||
BorderPane container = new BorderPane();
|
BorderPane container = new BorderPane();
|
||||||
container.setPadding(new Insets(10));
|
container.setPadding(new Insets(10));
|
||||||
container.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(0))));
|
|
||||||
setContent(container);
|
setContent(container);
|
||||||
|
|
||||||
VBox headerVbox = new VBox(10);
|
VBox headerVbox = new VBox(10);
|
||||||
|
@ -53,21 +48,21 @@ public class DonateTabFx extends Tab {
|
||||||
tokenDesc.setTextAlignment(TextAlignment.CENTER);
|
tokenDesc.setTextAlignment(TextAlignment.CENTER);
|
||||||
tokenBox.getChildren().addAll(tokenImage, tokenButton, tokenDesc);
|
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");
|
Button coffeeButton = new Button("Buy me a coffee");
|
||||||
coffeeButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.buymeacoffee.com/0xboobface"); });
|
coffeeButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.buymeacoffee.com/0xboobface"); });
|
||||||
VBox buyCoffeeBox = new VBox(5);
|
VBox buyCoffeeBox = new VBox(5);
|
||||||
buyCoffeeBox.setAlignment(Pos.TOP_CENTER);
|
buyCoffeeBox.setAlignment(Pos.TOP_CENTER);
|
||||||
buyCoffeeBox.getChildren().addAll(coffeeImage, coffeeButton);
|
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");
|
Button paypalButton = new Button("PayPal");
|
||||||
paypalButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.paypal.me/0xb00bface"); });
|
paypalButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.paypal.me/0xb00bface"); });
|
||||||
VBox paypalBox = new VBox(5);
|
VBox paypalBox = new VBox(5);
|
||||||
paypalBox.setAlignment(Pos.TOP_CENTER);
|
paypalBox.setAlignment(Pos.TOP_CENTER);
|
||||||
paypalBox.getChildren().addAll(paypalImage, paypalButton);
|
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");
|
Button patreonButton = new Button("Become a Patron");
|
||||||
patreonButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.patreon.com/0xb00bface"); });
|
patreonButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.patreon.com/0xb00bface"); });
|
||||||
VBox patreonBox = new VBox(5);
|
VBox patreonBox = new VBox(5);
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class JavaFxModel implements Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
return delegate.getOnlineState(failFast);
|
return delegate.getOnlineState(failFast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,4 +197,19 @@ public class JavaFxModel implements Model {
|
||||||
delegate.setSuspended(suspended);
|
delegate.setSuspended(suspended);
|
||||||
pausedProperty.set(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;
|
package ctbrec.ui;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
|
|
||||||
|
@ -12,9 +13,10 @@ public class JavaFxRecording extends Recording {
|
||||||
|
|
||||||
private transient StringProperty statusProperty = new SimpleStringProperty();
|
private transient StringProperty statusProperty = new SimpleStringProperty();
|
||||||
private transient StringProperty progressProperty = new SimpleStringProperty();
|
private transient StringProperty progressProperty = new SimpleStringProperty();
|
||||||
private transient StringProperty sizeProperty = new SimpleStringProperty();
|
private transient LongProperty sizeProperty = new SimpleLongProperty();
|
||||||
|
|
||||||
private Recording delegate;
|
private Recording delegate;
|
||||||
|
private long lastValue = 0;
|
||||||
|
|
||||||
public JavaFxRecording(Recording recording) {
|
public JavaFxRecording(Recording recording) {
|
||||||
this.delegate = recording;
|
this.delegate = recording;
|
||||||
|
@ -41,7 +43,7 @@ public class JavaFxRecording extends Recording {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public STATUS getStatus() {
|
public State getStatus() {
|
||||||
return delegate.getStatus();
|
return delegate.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ public class JavaFxRecording extends Recording {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setStatus(STATUS status) {
|
public void setStatus(State status) {
|
||||||
delegate.setStatus(status);
|
delegate.setStatus(status);
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case RECORDING:
|
case RECORDING:
|
||||||
|
@ -65,8 +67,14 @@ public class JavaFxRecording extends Recording {
|
||||||
case DOWNLOADING:
|
case DOWNLOADING:
|
||||||
statusProperty.set("downloading");
|
statusProperty.set("downloading");
|
||||||
break;
|
break;
|
||||||
case MERGING:
|
case POST_PROCESSING:
|
||||||
statusProperty.set("merging");
|
statusProperty.set("post-processing");
|
||||||
|
break;
|
||||||
|
case STOPPED:
|
||||||
|
statusProperty.set("stopped");
|
||||||
|
break;
|
||||||
|
case UNKNOWN:
|
||||||
|
statusProperty.set("unknown");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,9 +97,7 @@ public class JavaFxRecording extends Recording {
|
||||||
@Override
|
@Override
|
||||||
public void setSizeInByte(long sizeInByte) {
|
public void setSizeInByte(long sizeInByte) {
|
||||||
delegate.setSizeInByte(sizeInByte);
|
delegate.setSizeInByte(sizeInByte);
|
||||||
double sizeInGiB = sizeInByte / 1024.0 / 1024 / 1024;
|
sizeProperty.set(sizeInByte);
|
||||||
DecimalFormat df = new DecimalFormat("0.00");
|
|
||||||
sizeProperty.setValue(df.format(sizeInGiB) + " GiB");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty getProgressProperty() {
|
public StringProperty getProgressProperty() {
|
||||||
|
@ -115,7 +121,7 @@ public class JavaFxRecording extends Recording {
|
||||||
|
|
||||||
public void update(Recording updated) {
|
public void update(Recording updated) {
|
||||||
if(!Config.getInstance().getSettings().localRecording) {
|
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
|
// ignore, because the the status coming from the server is FINISHED and we are
|
||||||
// overriding it with DOWNLOADING
|
// overriding it with DOWNLOADING
|
||||||
return;
|
return;
|
||||||
|
@ -151,8 +157,13 @@ public class JavaFxRecording extends Recording {
|
||||||
return delegate.getSizeInByte();
|
return delegate.getSizeInByte();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty getSizeProperty() {
|
public LongProperty getSizeProperty() {
|
||||||
return sizeProperty;
|
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 final transient Logger LOG = LoggerFactory.getLogger(Player.class);
|
||||||
private static PlayerThread playerThread;
|
private static PlayerThread playerThread;
|
||||||
|
|
||||||
public static void play(String url) {
|
public static boolean play(String url) {
|
||||||
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
|
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
|
||||||
try {
|
try {
|
||||||
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
|
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
|
||||||
|
@ -31,12 +31,14 @@ public class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
playerThread = new PlayerThread(url);
|
playerThread = new PlayerThread(url);
|
||||||
|
return true;
|
||||||
} catch (Exception e1) {
|
} catch (Exception e1) {
|
||||||
LOG.error("Couldn't start player", 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;
|
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
|
||||||
try {
|
try {
|
||||||
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
|
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
|
||||||
|
@ -44,12 +46,14 @@ public class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
playerThread = new PlayerThread(rec);
|
playerThread = new PlayerThread(rec);
|
||||||
|
return true;
|
||||||
} catch (Exception e1) {
|
} catch (Exception e1) {
|
||||||
LOG.error("Couldn't start player", e1);
|
LOG.error("Couldn't start player", e1);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void play(Model model) {
|
public static boolean play(Model model) {
|
||||||
try {
|
try {
|
||||||
if(model.isOnline(true)) {
|
if(model.isOnline(true)) {
|
||||||
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
|
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
|
||||||
|
@ -60,7 +64,7 @@ public class Player {
|
||||||
Collections.sort(sources);
|
Collections.sort(sources);
|
||||||
StreamSource best = sources.get(sources.size()-1);
|
StreamSource best = sources.get(sources.size()-1);
|
||||||
LOG.debug("Playing {}", best.getMediaPlaylistUrl());
|
LOG.debug("Playing {}", best.getMediaPlaylistUrl());
|
||||||
Player.play(best.getMediaPlaylistUrl());
|
return Player.play(best.getMediaPlaylistUrl());
|
||||||
} else {
|
} else {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
||||||
|
@ -68,6 +72,7 @@ public class Player {
|
||||||
alert.setHeaderText("Room is currently not public");
|
alert.setHeaderText("Room is currently not public");
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e1) {
|
} catch (Exception e1) {
|
||||||
LOG.error("Couldn't get stream information for model {}", model, e1);
|
LOG.error("Couldn't get stream information for model {}", model, e1);
|
||||||
|
@ -78,6 +83,7 @@ public class Player {
|
||||||
alert.setContentText(e1.getLocalizedMessage());
|
alert.setContentText(e1.getLocalizedMessage());
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +120,11 @@ public class Player {
|
||||||
try {
|
try {
|
||||||
if (Config.getInstance().getSettings().localRecording && rec != null) {
|
if (Config.getInstance().getSettings().localRecording && rec != null) {
|
||||||
File file = new File(Config.getInstance().getSettings().recordingsDir, rec.getPath());
|
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 {
|
} else {
|
||||||
if(Config.getInstance().getSettings().requireAuthentication) {
|
if(Config.getInstance().getSettings().requireAuthentication) {
|
||||||
URL u = new URL(url);
|
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,
|
// 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
|
// 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(), new DevNull()));
|
||||||
|
//Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out));
|
||||||
std.setName("Player stdout pipe");
|
std.setName("Player stdout pipe");
|
||||||
std.setDaemon(true);
|
std.setDaemon(true);
|
||||||
std.start();
|
std.start();
|
||||||
Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), new DevNull()));
|
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.setName("Player stderr pipe");
|
||||||
err.setDaemon(true);
|
err.setDaemon(true);
|
||||||
err.start();
|
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.io.IOException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -20,27 +19,36 @@ import java.util.stream.Collectors;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.ui.autofilltextbox.AutoFillTextField;
|
import ctbrec.ui.action.FollowAction;
|
||||||
import javafx.application.Platform;
|
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.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Cursor;
|
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TableColumn;
|
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.TableView;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.cell.CheckBoxTableCell;
|
import javafx.scene.control.cell.CheckBoxTableCell;
|
||||||
|
@ -50,6 +58,7 @@ import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.input.ContextMenuEvent;
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.FlowPane;
|
import javafx.scene.layout.FlowPane;
|
||||||
|
@ -59,9 +68,6 @@ import javafx.util.Duration;
|
||||||
public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
|
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 ScheduledService<List<JavaFxModel>> updateService;
|
||||||
private Recorder recorder;
|
private Recorder recorder;
|
||||||
private List<Site> sites;
|
private List<Site> sites;
|
||||||
|
@ -75,6 +81,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
Label modelLabel = new Label("Model");
|
Label modelLabel = new Label("Model");
|
||||||
AutoFillTextField model;
|
AutoFillTextField model;
|
||||||
Button addModelButton = new Button("Record");
|
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) {
|
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
|
||||||
super(title);
|
super(title);
|
||||||
|
@ -96,42 +104,74 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
scrollPane.setFitToWidth(true);
|
scrollPane.setFitToWidth(true);
|
||||||
BorderPane.setMargin(scrollPane, new Insets(5));
|
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");
|
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
|
||||||
name.setPrefWidth(200);
|
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");
|
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
|
||||||
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url"));
|
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url"));
|
||||||
url.setPrefWidth(400);
|
url.setPrefWidth(400);
|
||||||
|
url.setEditable(false);
|
||||||
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
|
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.setCellFactory(CheckBoxTableCell.forTableColumn(online));
|
||||||
online.setPrefWidth(100);
|
online.setPrefWidth(100);
|
||||||
|
online.setEditable(false);
|
||||||
TableColumn<JavaFxModel, Boolean> recording = new TableColumn<>("Recording");
|
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.setCellFactory(CheckBoxTableCell.forTableColumn(recording));
|
||||||
recording.setPrefWidth(100);
|
recording.setPrefWidth(100);
|
||||||
|
recording.setEditable(false);
|
||||||
TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused");
|
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.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
|
||||||
paused.setPrefWidth(100);
|
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.setItems(observableModels);
|
||||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||||
popup = createContextMenu();
|
popup = createContextMenu();
|
||||||
if(popup != null) {
|
if (popup != null) {
|
||||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||||
}
|
}
|
||||||
event.consume();
|
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 -> {
|
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||||
if(popup != null) {
|
if (popup != null) {
|
||||||
popup.hide();
|
popup.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||||
if(event.getCode() == KeyCode.DELETE) {
|
List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
|
||||||
stopAction();
|
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);
|
scrollPane.setContent(table);
|
||||||
|
@ -141,23 +181,65 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
ObservableList<String> suggestions = FXCollections.observableArrayList();
|
ObservableList<String> suggestions = FXCollections.observableArrayList();
|
||||||
sites.forEach(site -> suggestions.add(site.getName()));
|
sites.forEach(site -> suggestions.add(site.getName()));
|
||||||
model = new AutoFillTextField(suggestions);
|
model = new AutoFillTextField(suggestions);
|
||||||
model.setPrefWidth(300);
|
model.setPrefWidth(600);
|
||||||
model.setPromptText("e.g. MyFreeCams:ModelName");
|
model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/");
|
||||||
model.onActionHandler(e -> addModel(e));
|
model.onActionHandler(this::addModel);
|
||||||
model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" +
|
model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" +
|
||||||
"press ENTER to confirm a suggested site name"));
|
"press ENTER to confirm a suggested site name"));
|
||||||
BorderPane.setMargin(addModelBox, new Insets(5));
|
BorderPane.setMargin(addModelBox, new Insets(5));
|
||||||
addModelButton.setOnAction((e) -> addModel(e));
|
addModelButton.setOnAction(this::addModel);
|
||||||
addModelBox.getChildren().addAll(modelLabel, model, addModelButton);
|
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();
|
BorderPane root = new BorderPane();
|
||||||
root.setPadding(new Insets(5));
|
root.setPadding(new Insets(5));
|
||||||
root.setTop(addModelBox);
|
root.setTop(addModelBox);
|
||||||
root.setCenter(scrollPane);
|
root.setCenter(scrollPane);
|
||||||
setContent(root);
|
setContent(root);
|
||||||
|
|
||||||
|
restoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addModel(ActionEvent e) {
|
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(":");
|
String[] parts = model.getText().trim().split(":");
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
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.setHeaderText("Couldn't add model");
|
||||||
alert.setContentText("The site you entered is unknown");
|
alert.setContentText("The site you entered is unknown");
|
||||||
alert.showAndWait();
|
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() {
|
void initializeUpdateService() {
|
||||||
updateService = createUpdateService();
|
updateService = createUpdateService();
|
||||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||||
updateService.setOnSucceeded((event) -> {
|
updateService.setOnSucceeded((event) -> {
|
||||||
List<JavaFxModel> models = updateService.getValue();
|
List<JavaFxModel> models = updateService.getValue();
|
||||||
if(models == null) {
|
if (models == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,6 +296,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
int index = observableModels.indexOf(updatedModel);
|
int index = observableModels.indexOf(updatedModel);
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
observableModels.add(updatedModel);
|
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 {
|
} else {
|
||||||
// make sure to update the JavaFX online property, so that the table cell is updated
|
// make sure to update the JavaFX online property, so that the table cell is updated
|
||||||
JavaFxModel oldModel = observableModels.get(index);
|
JavaFxModel oldModel = observableModels.get(index);
|
||||||
|
@ -222,6 +322,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.sort();
|
||||||
});
|
});
|
||||||
updateService.setOnFailed((event) -> {
|
updateService.setOnFailed((event) -> {
|
||||||
LOG.info("Couldn't get list of models from recorder", event.getSource().getException());
|
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))
|
.map(m -> new JavaFxModel(m))
|
||||||
.peek(fxm -> {
|
.peek(fxm -> {
|
||||||
for (Recording recording : recordings) {
|
for (Recording recording : recordings) {
|
||||||
if(recording.getStatus() == Recording.STATUS.RECORDING &&
|
if(recording.getStatus() == Recording.State.RECORDING &&
|
||||||
recording.getModelName().equals(fxm.getName()))
|
recording.getModelName().equals(fxm.getName()))
|
||||||
{
|
{
|
||||||
fxm.getRecordingProperty().set(true);
|
fxm.getRecordingProperty().set(true);
|
||||||
|
@ -292,16 +394,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContextMenu createContextMenu() {
|
private ContextMenu createContextMenu() {
|
||||||
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
|
ObservableList<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
|
||||||
if(selectedModel == null) {
|
if (selectedModels.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
MenuItem stop = new MenuItem("Remove Model");
|
MenuItem stop = new MenuItem("Remove Model");
|
||||||
stop.setOnAction((e) -> stopAction());
|
stop.setOnAction((e) -> stopAction(selectedModels));
|
||||||
|
|
||||||
MenuItem copyUrl = new MenuItem("Copy URL");
|
MenuItem copyUrl = new MenuItem("Copy URL");
|
||||||
copyUrl.setOnAction((e) -> {
|
copyUrl.setOnAction((e) -> {
|
||||||
Model selected = selectedModel;
|
Model selected = selectedModels.get(0);
|
||||||
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||||
final ClipboardContent content = new ClipboardContent();
|
final ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(selected.getUrl());
|
content.putString(selected.getUrl());
|
||||||
|
@ -309,33 +411,47 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
});
|
});
|
||||||
|
|
||||||
MenuItem pauseRecording = new MenuItem("Pause Recording");
|
MenuItem pauseRecording = new MenuItem("Pause Recording");
|
||||||
pauseRecording.setOnAction((e) -> pauseRecording());
|
pauseRecording.setOnAction((e) -> pauseRecording(selectedModels));
|
||||||
MenuItem resumeRecording = new MenuItem("Resume Recording");
|
MenuItem resumeRecording = new MenuItem("Resume Recording");
|
||||||
resumeRecording.setOnAction((e) -> resumeRecording());
|
resumeRecording.setOnAction((e) -> resumeRecording(selectedModels));
|
||||||
MenuItem openInBrowser = new MenuItem("Open in Browser");
|
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");
|
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");
|
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);
|
ContextMenu menu = new ContextMenu(stop);
|
||||||
menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording);
|
if (selectedModels.size() == 1) {
|
||||||
menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource);
|
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;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void follow(ObservableList<JavaFxModel> selectedModels) {
|
||||||
|
new FollowAction(getTabPane(), new ArrayList<JavaFxModel>(selectedModels)).execute();
|
||||||
|
}
|
||||||
|
|
||||||
private void openInPlayer(JavaFxModel selectedModel) {
|
private void openInPlayer(JavaFxModel selectedModel) {
|
||||||
table.setCursor(Cursor.WAIT);
|
new PlayAction(getTabPane(), selectedModel).execute();
|
||||||
new Thread(() -> {
|
|
||||||
Player.play(selectedModel);
|
|
||||||
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void switchStreamSource(JavaFxModel fxModel) {
|
private void switchStreamSource(JavaFxModel fxModel) {
|
||||||
try {
|
try {
|
||||||
if(!fxModel.isOnline()) {
|
if (!fxModel.isOnline()) {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
||||||
alert.setTitle("Switch resolution");
|
alert.setTitle("Switch resolution");
|
||||||
alert.setHeaderText("Couldn't switch stream resolution");
|
alert.setHeaderText("Couldn't switch stream resolution");
|
||||||
|
@ -370,93 +486,65 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showStreamSwitchErrorDialog(Throwable throwable) {
|
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 alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||||
alert.setTitle("Error");
|
alert.setTitle("Error");
|
||||||
alert.setHeaderText("Couldn't switch stream resolution");
|
alert.setHeaderText(header);
|
||||||
alert.setContentText("Error while switching stream resolution: " + throwable.getLocalizedMessage());
|
alert.setContentText(msg + ": " + throwable.getLocalizedMessage());
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopAction() {
|
private void stopAction(List<JavaFxModel> selectedModels) {
|
||||||
Model selected = table.getSelectionModel().getSelectedItem().getDelegate();
|
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
|
||||||
if (selected != null) {
|
new StopRecordingAction(getTabPane(), models, recorder).execute((m) -> {
|
||||||
table.setCursor(Cursor.WAIT);
|
observableModels.remove(m);
|
||||||
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();
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
table.setCursor(Cursor.DEFAULT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private void pauseRecording() {
|
private void pauseRecording(List<JavaFxModel> selectedModels) {
|
||||||
JavaFxModel model = table.getSelectionModel().getSelectedItem();
|
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
|
||||||
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
|
new PauseAction(getTabPane(), models, recorder).execute();
|
||||||
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 resumeRecording() {
|
private void resumeRecording(List<JavaFxModel> selectedModels) {
|
||||||
JavaFxModel model = table.getSelectionModel().getSelectedItem();
|
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
|
||||||
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
|
new ResumeAction(getTabPane(), models, recorder).execute();
|
||||||
if (delegate != null) {
|
}
|
||||||
table.setCursor(Cursor.WAIT);
|
|
||||||
new Thread() {
|
public void saveState() {
|
||||||
@Override
|
if (!table.getSortOrder().isEmpty()) {
|
||||||
public void run() {
|
TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0);
|
||||||
try {
|
Config.getInstance().getSettings().recordedModelsSortColumn = col.getText();
|
||||||
recorder.resumeRecording(delegate);
|
Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.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;
|
package ctbrec.ui;
|
||||||
|
|
||||||
|
import static ctbrec.Recording.State.*;
|
||||||
import static javafx.scene.control.ButtonType.*;
|
import static javafx.scene.control.ButtonType.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
@ -16,10 +19,13 @@ import java.time.format.FormatStyle;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -29,10 +35,12 @@ import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.Recording.STATUS;
|
import ctbrec.Recording.State;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.recorder.download.MergedHlsDownload;
|
import ctbrec.recorder.download.MergedHlsDownload;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
import ctbrec.ui.controls.Toast;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
|
@ -44,19 +52,28 @@ import javafx.scene.Cursor;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.ProgressBar;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TableCell;
|
import javafx.scene.control.TableCell;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
|
import javafx.scene.control.TableColumn.SortType;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
import javafx.scene.input.ContextMenuEvent;
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.FlowPane;
|
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.stage.FileChooser;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
@ -69,12 +86,17 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
private Recorder recorder;
|
private Recorder recorder;
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private List<Site> sites;
|
private List<Site> sites;
|
||||||
|
private long spaceTotal = -1;
|
||||||
|
private long spaceFree = -1;
|
||||||
|
|
||||||
FlowPane grid = new FlowPane();
|
FlowPane grid = new FlowPane();
|
||||||
ScrollPane scrollPane = new ScrollPane();
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
|
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
|
||||||
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
|
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
|
||||||
ContextMenu popup;
|
ContextMenu popup;
|
||||||
|
ProgressBar spaceLeft;
|
||||||
|
Label spaceLabel;
|
||||||
|
Lock recordingsLock = new ReentrantLock();
|
||||||
|
|
||||||
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
|
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
|
||||||
super(title);
|
super(title);
|
||||||
|
@ -98,6 +120,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
BorderPane.setMargin(scrollPane, new Insets(5));
|
BorderPane.setMargin(scrollPane, new Insets(5));
|
||||||
|
|
||||||
table.setEditable(false);
|
table.setEditable(false);
|
||||||
|
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
|
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
|
||||||
name.setPrefWidth(200);
|
name.setPrefWidth(200);
|
||||||
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
|
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");
|
TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
|
||||||
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
|
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
|
||||||
progress.setPrefWidth(100);
|
progress.setPrefWidth(100);
|
||||||
TableColumn<JavaFxRecording, String> size = new TableColumn<>("Size");
|
TableColumn<JavaFxRecording, Number> size = new TableColumn<>("Size");
|
||||||
size.setCellValueFactory((cdf) -> cdf.getValue().getSizeProperty());
|
size.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||||
size.setPrefWidth(100);
|
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.getColumns().addAll(name, date, status, progress, size);
|
||||||
table.setItems(observableRecordings);
|
table.setItems(observableRecordings);
|
||||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||||
Recording recording = table.getSelectionModel().getSelectedItem();
|
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||||
if(recording != null) {
|
if(recordings != null && !recordings.isEmpty()) {
|
||||||
popup = createContextMenu(recording);
|
popup = createContextMenu(recordings);
|
||||||
if(!popup.getItems().isEmpty()) {
|
if(!popup.getItems().isEmpty()) {
|
||||||
popup.show(table, event.getScreenX(), event.getScreenY());
|
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||||
}
|
}
|
||||||
|
@ -152,35 +201,89 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
popup.hide();
|
popup.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
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 -> {
|
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||||
JavaFxRecording recording = table.getSelectionModel().getSelectedItem();
|
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||||
if (recording != null) {
|
if (recordings != null && !recordings.isEmpty()) {
|
||||||
if (event.getCode() == KeyCode.DELETE) {
|
if (event.getCode() == KeyCode.DELETE) {
|
||||||
delete(recording);
|
if(recordings.size() > 1 || recordings.get(0).getStatus() == State.FINISHED) {
|
||||||
|
delete(recordings);
|
||||||
|
}
|
||||||
} else if (event.getCode() == KeyCode.ENTER) {
|
} else if (event.getCode() == KeyCode.ENTER) {
|
||||||
if(recording.getStatus() == STATUS.FINISHED) {
|
if(recordings.get(0).getStatus() == State.FINISHED) {
|
||||||
play(recording);
|
play(recordings.get(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
scrollPane.setContent(table);
|
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();
|
BorderPane root = new BorderPane();
|
||||||
root.setPadding(new Insets(5));
|
root.setPadding(new Insets(5));
|
||||||
|
root.setTop(spaceBox);
|
||||||
root.setCenter(scrollPane);
|
root.setCenter(scrollPane);
|
||||||
setContent(root);
|
setContent(root);
|
||||||
|
|
||||||
|
restoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeUpdateService() {
|
void initializeUpdateService() {
|
||||||
updateService = createUpdateService();
|
updateService = createUpdateService();
|
||||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||||
updateService.setOnSucceeded((event) -> {
|
updateService.setOnSucceeded((event) -> {
|
||||||
List<JavaFxRecording> recordings = updateService.getValue();
|
updateRecordingsTable();
|
||||||
if (recordings == null) {
|
updateFreeSpaceDisplay();
|
||||||
return;
|
});
|
||||||
}
|
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();) {
|
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
|
||||||
JavaFxRecording old = iterator.next();
|
JavaFxRecording old = iterator.next();
|
||||||
if (!recordings.contains(old)) {
|
if (!recordings.contains(old)) {
|
||||||
|
@ -199,16 +302,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
old.update(recording);
|
old.update(recording);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
table.sort();
|
} finally {
|
||||||
});
|
recordingsLock.unlock();
|
||||||
updateService.setOnFailed((event) -> {
|
}
|
||||||
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
|
table.sort();
|
||||||
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() {
|
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
|
||||||
|
@ -218,12 +315,27 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
return new Task<List<JavaFxRecording>>() {
|
return new Task<List<JavaFxRecording>>() {
|
||||||
@Override
|
@Override
|
||||||
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||||
|
updateSpace();
|
||||||
|
|
||||||
List<JavaFxRecording> recordings = new ArrayList<>();
|
List<JavaFxRecording> recordings = new ArrayList<>();
|
||||||
for (Recording rec : recorder.getRecordings()) {
|
for (Recording rec : recorder.getRecordings()) {
|
||||||
recordings.add(new JavaFxRecording(rec));
|
recordings.add(new JavaFxRecording(rec));
|
||||||
}
|
}
|
||||||
return recordings;
|
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 contextMenu = new ContextMenu();
|
||||||
contextMenu.setHideOnEscape(true);
|
contextMenu.setHideOnEscape(true);
|
||||||
contextMenu.setAutoHide(true);
|
contextMenu.setAutoHide(true);
|
||||||
|
@ -263,9 +375,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||||
openInPlayer.setOnAction((e) -> {
|
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);
|
contextMenu.getItems().add(openInPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,16 +397,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
MenuItem deleteRecording = new MenuItem("Delete");
|
MenuItem deleteRecording = new MenuItem("Delete");
|
||||||
deleteRecording.setOnAction((e) -> {
|
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);
|
contextMenu.getItems().add(deleteRecording);
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem openDir = new MenuItem("Open directory");
|
MenuItem openDir = new MenuItem("Open directory");
|
||||||
openDir.setOnAction((e) -> {
|
openDir.setOnAction((e) -> {
|
||||||
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
|
||||||
String path = recording.getPath();
|
String path = recordings.get(0).getPath();
|
||||||
File tsFile = new File(recordingsDir, path);
|
File tsFile = new File(recordingsDir, path);
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
DesktopIntegration.open(tsFile.getParent());
|
DesktopIntegration.open(tsFile.getParent());
|
||||||
|
@ -307,16 +419,22 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
MenuItem downloadRecording = new MenuItem("Download");
|
MenuItem downloadRecording = new MenuItem("Download");
|
||||||
downloadRecording.setOnAction((e) -> {
|
downloadRecording.setOnAction((e) -> {
|
||||||
try {
|
try {
|
||||||
download(recording);
|
download(recordings.get(0));
|
||||||
} catch (IOException | ParseException | PlaylistException e1) {
|
} catch (IOException | ParseException | PlaylistException e1) {
|
||||||
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1);
|
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1);
|
||||||
LOG.error("Error while downloading recording", 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);
|
contextMenu.getItems().add(downloadRecording);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(recordings.size() > 1) {
|
||||||
|
openInPlayer.setDisable(true);
|
||||||
|
openDir.setDisable(true);
|
||||||
|
downloadRecording.setDisable(true);
|
||||||
|
}
|
||||||
|
|
||||||
return contextMenu;
|
return contextMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,11 +464,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
download.start(url.toString(), target, (progress) -> {
|
download.start(url.toString(), target, (progress) -> {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (progress == 100) {
|
if (progress == 100) {
|
||||||
recording.setStatus(STATUS.FINISHED);
|
recording.setStatus(FINISHED);
|
||||||
recording.setProgress(-1);
|
recording.setProgress(-1);
|
||||||
LOG.debug("Download finished for recording {}", recording.getPath());
|
LOG.debug("Download finished for recording {}", recording.getPath());
|
||||||
} else {
|
} else {
|
||||||
recording.setStatus(STATUS.DOWNLOADING);
|
recording.setStatus(DOWNLOADING);
|
||||||
recording.setProgress(progress);
|
recording.setProgress(progress);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -365,7 +483,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
Platform.runLater(new Runnable() {
|
Platform.runLater(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
recording.setStatus(STATUS.FINISHED);
|
recording.setStatus(FINISHED);
|
||||||
recording.setProgress(-1);
|
recording.setProgress(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -376,85 +494,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
t.setName("Download Thread " + recording.getPath());
|
t.setName("Download Thread " + recording.getPath());
|
||||||
t.start();
|
t.start();
|
||||||
|
|
||||||
recording.setStatus(STATUS.DOWNLOADING);
|
recording.setStatus(State.DOWNLOADING);
|
||||||
recording.setProgress(0);
|
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) {
|
private void showErrorDialog(final String title, final String msg, final Exception e) {
|
||||||
Platform.runLater(new Runnable() {
|
Platform.runLater(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -474,7 +518,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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();
|
}.start();
|
||||||
} else {
|
} else {
|
||||||
|
@ -483,19 +530,26 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void delete(Recording r) {
|
private void delete(List<JavaFxRecording> recordings) {
|
||||||
if(r.getStatus() != STATUS.FINISHED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
table.setCursor(Cursor.WAIT);
|
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);
|
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, YES, NO);
|
||||||
confirm.setTitle("Delete recording?");
|
confirm.setTitle("Delete recording?");
|
||||||
confirm.setHeaderText(msg);
|
confirm.setHeaderText(msg);
|
||||||
|
@ -505,14 +559,26 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
Thread deleteThread = new Thread() {
|
Thread deleteThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
recordingsLock.lock();
|
||||||
try {
|
try {
|
||||||
recorder.delete(r);
|
List<Recording> deleted = new ArrayList<>();
|
||||||
Platform.runLater(() -> observableRecordings.remove(r));
|
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
|
||||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
JavaFxRecording r = iterator.next();
|
||||||
LOG.error("Error while deleting recording", e1);
|
if(r.getStatus() != FINISHED) {
|
||||||
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
recorder.delete(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 {
|
} 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);
|
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;
|
package ctbrec.ui;
|
||||||
|
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.layout.BorderPane;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
|
|
||||||
public class SiteTab extends Tab implements TabSelectionListener {
|
public class SiteTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
private BorderPane rootPane = new BorderPane();
|
|
||||||
private HBox tokenPanel;
|
|
||||||
private SiteTabPane siteTabPane;
|
private SiteTabPane siteTabPane;
|
||||||
|
|
||||||
public SiteTab(Site site, Scene scene) {
|
public SiteTab(Site site, Scene scene) {
|
||||||
super(site.getName());
|
super(site.getName());
|
||||||
|
|
||||||
setClosable(false);
|
setClosable(false);
|
||||||
setContent(rootPane);
|
|
||||||
siteTabPane = new SiteTabPane(site, scene);
|
siteTabPane = new SiteTabPane(site, scene);
|
||||||
rootPane.setCenter(siteTabPane);
|
setContent(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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,12 +7,14 @@ import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.fc2live.Fc2Live;
|
import ctbrec.sites.fc2live.Fc2Live;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
|
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
|
||||||
import ctbrec.ui.sites.cam4.Cam4SiteUi;
|
import ctbrec.ui.sites.cam4.Cam4SiteUi;
|
||||||
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
|
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
|
||||||
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
|
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
|
||||||
import ctbrec.ui.sites.fc2live.Fc2LiveUi;
|
import ctbrec.ui.sites.fc2live.Fc2LiveUi;
|
||||||
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
|
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
|
||||||
|
import ctbrec.ui.sites.streamate.StreamateSiteUi;
|
||||||
|
|
||||||
public class SiteUiFactory {
|
public class SiteUiFactory {
|
||||||
|
|
||||||
|
@ -22,8 +24,9 @@ public class SiteUiFactory {
|
||||||
private static ChaturbateSiteUi ctbSiteUi;
|
private static ChaturbateSiteUi ctbSiteUi;
|
||||||
private static Fc2LiveUi fc2SiteUi;
|
private static Fc2LiveUi fc2SiteUi;
|
||||||
private static MyFreeCamsSiteUi mfcSiteUi;
|
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 (site instanceof BongaCams) {
|
||||||
if (bongaSiteUi == null) {
|
if (bongaSiteUi == null) {
|
||||||
bongaSiteUi = new BongaCamsSiteUi((BongaCams) site);
|
bongaSiteUi = new BongaCamsSiteUi((BongaCams) site);
|
||||||
|
@ -54,6 +57,11 @@ public class SiteUiFactory {
|
||||||
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);
|
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);
|
||||||
}
|
}
|
||||||
return mfcSiteUi;
|
return mfcSiteUi;
|
||||||
|
} else if (site instanceof Streamate) {
|
||||||
|
if (streamateSiteUi == null) {
|
||||||
|
streamateSiteUi = new StreamateSiteUi((Streamate) site);
|
||||||
|
}
|
||||||
|
return streamateSiteUi;
|
||||||
}
|
}
|
||||||
throw new RuntimeException("Unknown site " + site.getName());
|
throw new RuntimeException("Unknown site " + site.getName());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.ui;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
@ -15,14 +16,17 @@ public class StreamSourceSelectionDialog {
|
||||||
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
|
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
|
||||||
@Override
|
@Override
|
||||||
protected List<StreamSource> call() throws Exception {
|
protected List<StreamSource> call() throws Exception {
|
||||||
return model.getStreamSources();
|
List<StreamSource> sources = model.getStreamSources();
|
||||||
|
Collections.sort(sources);
|
||||||
|
return sources;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
selectStreamSource.setOnSucceeded((e) -> {
|
selectStreamSource.setOnSucceeded((e) -> {
|
||||||
List<StreamSource> sources;
|
List<StreamSource> sources;
|
||||||
try {
|
try {
|
||||||
sources = selectStreamSource.get();
|
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.setTitle("Stream Quality");
|
||||||
choiceDialog.setHeaderText("Select your preferred stream quality");
|
choiceDialog.setHeaderText("Select your preferred stream quality");
|
||||||
choiceDialog.setResizable(true);
|
choiceDialog.setResizable(true);
|
||||||
|
|
|
@ -5,16 +5,24 @@ import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
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 java.util.function.Function;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
|
import ctbrec.ui.action.PlayAction;
|
||||||
|
import ctbrec.ui.controls.StreamPreview;
|
||||||
import javafx.animation.FadeTransition;
|
import javafx.animation.FadeTransition;
|
||||||
import javafx.animation.FillTransition;
|
import javafx.animation.FillTransition;
|
||||||
import javafx.animation.ParallelTransition;
|
import javafx.animation.ParallelTransition;
|
||||||
|
@ -36,12 +44,15 @@ import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.paint.Paint;
|
import javafx.scene.paint.Paint;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
|
import javafx.scene.shape.Polygon;
|
||||||
import javafx.scene.shape.Rectangle;
|
import javafx.scene.shape.Rectangle;
|
||||||
import javafx.scene.shape.Shape;
|
import javafx.scene.shape.Shape;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.scene.text.TextAlignment;
|
import javafx.scene.text.TextAlignment;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
public class ThumbCell extends StackPane {
|
public class ThumbCell extends StackPane {
|
||||||
|
|
||||||
|
@ -49,6 +60,7 @@ public class ThumbCell extends StackPane {
|
||||||
private static final Duration ANIMATION_DURATION = new Duration(250);
|
private static final Duration ANIMATION_DURATION = new Duration(250);
|
||||||
|
|
||||||
private Model model;
|
private Model model;
|
||||||
|
private StreamPreview streamPreview;
|
||||||
private ImageView iv;
|
private ImageView iv;
|
||||||
private Rectangle resolutionBackground;
|
private Rectangle resolutionBackground;
|
||||||
private final Paint resolutionOnlineColor = new Color(0.22, 0.8, 0.29, 1);
|
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 ObservableList<Node> thumbCellList;
|
||||||
private boolean mouseHovering = false;
|
private boolean mouseHovering = false;
|
||||||
private boolean recording = 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) {
|
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) {
|
||||||
|
this.parent = parent;
|
||||||
this.thumbCellList = parent.grid.getChildren();
|
this.thumbCellList = parent.grid.getChildren();
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.recorder = recorder;
|
this.recorder = recorder;
|
||||||
recording = recorder.isRecording(model);
|
recording = recorder.isRecording(model);
|
||||||
model.setSuspended(recorder.isSuspended(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 = new ImageView();
|
||||||
iv.setSmooth(true);
|
iv.setSmooth(true);
|
||||||
|
@ -109,7 +133,7 @@ public class ThumbCell extends StackPane {
|
||||||
StackPane.setMargin(resolutionBackground, new Insets(2));
|
StackPane.setMargin(resolutionBackground, new Insets(2));
|
||||||
getChildren().add(resolutionBackground);
|
getChildren().add(resolutionBackground);
|
||||||
|
|
||||||
name = new Text(model.getName());
|
name = new Text(model.getDisplayName());
|
||||||
name.setFill(Color.WHITE);
|
name.setFill(Color.WHITE);
|
||||||
name.setFont(new Font("Sansserif", 16));
|
name.setFont(new Font("Sansserif", 16));
|
||||||
name.setTextAlignment(TextAlignment.CENTER);
|
name.setTextAlignment(TextAlignment.CENTER);
|
||||||
|
@ -150,8 +174,14 @@ public class ThumbCell extends StackPane {
|
||||||
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
|
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
|
||||||
getChildren().add(pausedIndicator);
|
getChildren().add(pausedIndicator);
|
||||||
|
|
||||||
|
if(Config.getInstance().getSettings().previewInThumbnails) {
|
||||||
|
getChildren().add(createPreviewTrigger());
|
||||||
|
}
|
||||||
|
|
||||||
selectionOverlay = new Rectangle();
|
selectionOverlay = new Rectangle();
|
||||||
selectionOverlay.setOpacity(0);
|
selectionOverlay.visibleProperty().bind(selectionProperty);
|
||||||
|
selectionOverlay.widthProperty().bind(widthProperty());
|
||||||
|
selectionOverlay.heightProperty().bind(heightProperty());
|
||||||
StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT);
|
StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT);
|
||||||
getChildren().add(selectionOverlay);
|
getChildren().add(selectionOverlay);
|
||||||
|
|
||||||
|
@ -178,9 +208,51 @@ public class ThumbCell extends StackPane {
|
||||||
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
||||||
|
|
||||||
setRecording(recording);
|
setRecording(recording);
|
||||||
if(Config.getInstance().getSettings().determineResolution) {
|
update();
|
||||||
determineResolution();
|
}
|
||||||
|
|
||||||
|
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) {
|
public void setSelected(boolean selected) {
|
||||||
|
@ -203,48 +275,60 @@ public class ThumbCell extends StackPane {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ThumbOverviewTab.threadPool.submit(() -> {
|
int[] resolution = resolutionCache.getIfPresent(model);
|
||||||
try {
|
if(resolution != null) {
|
||||||
ThumbOverviewTab.resolutionProcessing.add(model);
|
ThumbOverviewTab.threadPool.submit(() -> {
|
||||||
int[] resolution = model.getStreamResolution(false);
|
try {
|
||||||
updateResolutionTag(resolution);
|
updateResolutionTag(resolution);
|
||||||
|
} catch(Exception e) {
|
||||||
// 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) {
|
|
||||||
LOG.trace("Removing invalid resolution value for {}", model.getName());
|
|
||||||
model.invalidateCacheEntries();
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.sleep(500);
|
|
||||||
} catch (IOException | InterruptedException e1) {
|
|
||||||
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
|
|
||||||
} catch(ExecutionException e) {
|
|
||||||
if(e.getCause() instanceof EOFException) {
|
|
||||||
LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName());
|
|
||||||
} else if(e.getCause() instanceof ParseException) {
|
|
||||||
LOG.warn("Couldn't update resolution tag for model {} - {}", model.getName(), e.getMessage());
|
|
||||||
} else {
|
|
||||||
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
|
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
|
||||||
}
|
}
|
||||||
} finally {
|
});
|
||||||
ThumbOverviewTab.resolutionProcessing.remove(model);
|
} else {
|
||||||
}
|
ThumbOverviewTab.threadPool.submit(() -> {
|
||||||
});
|
try {
|
||||||
|
ThumbOverviewTab.resolutionProcessing.add(model);
|
||||||
|
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) {
|
||||||
|
LOG.trace("Removing invalid resolution value for {}", model.getName());
|
||||||
|
model.invalidateCacheEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (IOException | InterruptedException e1) {
|
||||||
|
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
|
||||||
|
} catch(ExecutionException e) {
|
||||||
|
if(e.getCause() instanceof EOFException) {
|
||||||
|
LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName());
|
||||||
|
} else if(e.getCause() instanceof ParseException) {
|
||||||
|
LOG.warn("Couldn't update resolution tag for model {} - {}", model.getName(), e.getMessage());
|
||||||
|
} else {
|
||||||
|
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ThumbOverviewTab.resolutionProcessing.remove(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException, InterruptedException {
|
private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException, InterruptedException {
|
||||||
String _res = "n/a";
|
String _res = "n/a";
|
||||||
Paint resolutionBackgroundColor = resolutionOnlineColor;
|
Paint resolutionBackgroundColor = resolutionOnlineColor;
|
||||||
String state = model.getOnlineState(false);
|
String state = model.getOnlineState(false).toString();
|
||||||
if (model.isOnline()) {
|
if (model.isOnline()) {
|
||||||
LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]);
|
LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]);
|
||||||
LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size());
|
LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size());
|
||||||
final int w = resolution[1];
|
final int w = resolution[1];
|
||||||
_res = w > 0 ? w != Integer.MAX_VALUE ? Integer.toString(w) : "HD" : state;
|
_res = w > 0 ? w != Integer.MAX_VALUE ? Integer.toString(w) : "HD" : state;
|
||||||
} else {
|
} else {
|
||||||
_res = model.getOnlineState(false);
|
_res = model.getOnlineState(false).toString();
|
||||||
resolutionBackgroundColor = resolutionOfflineColor;
|
resolutionBackgroundColor = resolutionOfflineColor;
|
||||||
}
|
}
|
||||||
final String resText = _res;
|
final String resText = _res;
|
||||||
|
@ -262,20 +346,40 @@ public class ThumbCell extends StackPane {
|
||||||
|
|
||||||
private void setImage(String url) {
|
private void setImage(String url) {
|
||||||
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||||
Image img = new Image(url, true);
|
boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails;
|
||||||
|
if(updateThumbs || iv.getImage() == null) {
|
||||||
// wait for the image to load, otherwise the ImageView replaces the current image with an "empty" image,
|
imageLoadingThreadPool.submit(() -> {
|
||||||
// which causes to show the grey background until the image is loaded
|
Request req = new Request.Builder()
|
||||||
img.progressProperty().addListener(new ChangeListener<Number>() {
|
.url(url)
|
||||||
@Override
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
|
.build();
|
||||||
if(newValue.doubleValue() == 1.0) {
|
try(Response resp = CamrecApplication.httpClient.execute(req)) {
|
||||||
//imgAspectRatio = img.getHeight() / img.getWidth();
|
if(resp.isSuccessful()) {
|
||||||
iv.setImage(img);
|
Image img = new Image(resp.body().byteStream());
|
||||||
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
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) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,11 +401,7 @@ public class ThumbCell extends StackPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
void startPlayer() {
|
void startPlayer() {
|
||||||
setCursor(Cursor.WAIT);
|
new PlayAction(this, model).execute();
|
||||||
new Thread(() -> {
|
|
||||||
Player.play(model);
|
|
||||||
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRecording(boolean recording) {
|
private void setRecording(boolean recording) {
|
||||||
|
@ -314,6 +414,10 @@ public class ThumbCell extends StackPane {
|
||||||
nameBackground.setFill(c);
|
nameBackground.setFill(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateRecordingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRecordingIndicator() {
|
||||||
if(recording) {
|
if(recording) {
|
||||||
recordingIndicator.setVisible(!model.isSuspended());
|
recordingIndicator.setVisible(!model.isSuspended());
|
||||||
pausedIndicator.setVisible(model.isSuspended());
|
pausedIndicator.setVisible(model.isSuspended());
|
||||||
|
@ -458,7 +562,7 @@ public class ThumbCell extends StackPane {
|
||||||
this.model.setPreview(model.getPreview());
|
this.model.setPreview(model.getPreview());
|
||||||
this.model.setTags(model.getTags());
|
this.model.setTags(model.getTags());
|
||||||
this.model.setUrl(model.getUrl());
|
this.model.setUrl(model.getUrl());
|
||||||
this.model.setSuspended(model.isSuspended());
|
this.model.setSuspended(recorder.isSuspended(model));
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,7 +579,7 @@ public class ThumbCell extends StackPane {
|
||||||
setRecording(recorder.isRecording(model));
|
setRecording(recorder.isRecording(model));
|
||||||
setImage(model.getPreview());
|
setImage(model.getPreview());
|
||||||
String txt = recording ? " " : "";
|
String txt = recording ? " " : "";
|
||||||
txt += model.getDescription();
|
txt += model.getDescription() != null ? model.getDescription() : "";
|
||||||
topic.setText(txt);
|
topic.setText(txt);
|
||||||
|
|
||||||
if(Config.getInstance().getSettings().determineResolution) {
|
if(Config.getInstance().getSettings().determineResolution) {
|
||||||
|
@ -519,20 +623,31 @@ public class ThumbCell extends StackPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSize(int w, int h) {
|
private void setSize(int w, int h) {
|
||||||
iv.setFitWidth(w);
|
if(iv.getImage() != null) {
|
||||||
iv.setFitHeight(h);
|
double aspectRatio = iv.getImage().getWidth() / iv.getImage().getHeight();
|
||||||
|
if(aspectRatio > 1) {
|
||||||
|
iv.setFitWidth(w);
|
||||||
|
} else {
|
||||||
|
iv.setFitHeight(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
setMinSize(w, h);
|
setMinSize(w, h);
|
||||||
setPrefSize(w, h);
|
setPrefSize(w, h);
|
||||||
nameBackground.setWidth(w);
|
nameBackground.setWidth(w);
|
||||||
nameBackground.setHeight(20);
|
nameBackground.setHeight(20);
|
||||||
topicBackground.setWidth(w);
|
topicBackground.setWidth(w);
|
||||||
topicBackground.setHeight(getHeight()-nameBackground.getHeight());
|
topicBackground.setHeight(h - nameBackground.getHeight());
|
||||||
topic.prefHeight(getHeight()-25);
|
topic.prefHeight(getHeight()-25);
|
||||||
topic.maxHeight(getHeight()-25);
|
topic.maxHeight(getHeight()-25);
|
||||||
int margin = 4;
|
int margin = 4;
|
||||||
topic.maxWidth(w-margin*2);
|
topic.maxWidth(w-margin*2);
|
||||||
topic.setWrappingWidth(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;
|
package ctbrec.ui;
|
||||||
|
|
||||||
|
import static ctbrec.ui.controls.Dialogs.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
|
@ -26,18 +27,25 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
import ctbrec.event.EventBusHolder;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.sites.mfc.MyFreeCamsClient;
|
import ctbrec.sites.mfc.MyFreeCamsClient;
|
||||||
import ctbrec.sites.mfc.MyFreeCamsModel;
|
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.FadeTransition;
|
||||||
import javafx.animation.Interpolator;
|
import javafx.animation.Interpolator;
|
||||||
import javafx.animation.ParallelTransition;
|
import javafx.animation.ParallelTransition;
|
||||||
import javafx.animation.ScaleTransition;
|
import javafx.animation.ScaleTransition;
|
||||||
|
import javafx.animation.Transition;
|
||||||
import javafx.animation.TranslateTransition;
|
import javafx.animation.TranslateTransition;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
import javafx.concurrent.Worker.State;
|
import javafx.concurrent.Worker.State;
|
||||||
import javafx.concurrent.WorkerStateEvent;
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
|
@ -60,12 +68,16 @@ import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.input.ContextMenuEvent;
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.input.MouseButton;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.FlowPane;
|
import javafx.scene.layout.FlowPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.transform.Transform;
|
import javafx.scene.transform.Transform;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
|
@ -94,6 +106,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
ContextMenu popup;
|
ContextMenu popup;
|
||||||
Site site;
|
Site site;
|
||||||
StackPane root = new StackPane();
|
StackPane root = new StackPane();
|
||||||
|
Task<List<Model>> searchTask;
|
||||||
|
SearchPopover popover;
|
||||||
|
SearchPopoverTreeList popoverTreelist = new SearchPopoverTreeList();
|
||||||
|
|
||||||
private ComboBox<Integer> thumbWidth;
|
private ComboBox<Integer> thumbWidth;
|
||||||
|
|
||||||
|
@ -111,10 +126,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
grid.setHgap(5);
|
grid.setHgap(5);
|
||||||
grid.setVgap(5);
|
grid.setVgap(5);
|
||||||
|
|
||||||
TextField search = new TextField();
|
SearchBox filterInput = new SearchBox(false);
|
||||||
search.setPromptText("Filter models on this page");
|
filterInput.setPromptText("Filter models on this page");
|
||||||
search.textProperty().addListener( (observableValue, oldValue, newValue) -> {
|
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
|
||||||
filter = search.getText();
|
filter = filterInput.getText();
|
||||||
gridLock.lock();
|
gridLock.lock();
|
||||||
try {
|
try {
|
||||||
filter();
|
filter();
|
||||||
|
@ -123,12 +138,49 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
gridLock.unlock();
|
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"
|
+ "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\"");
|
+ "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.setContent(grid);
|
||||||
scrollPane.setFitToHeight(true);
|
scrollPane.setFitToHeight(true);
|
||||||
|
@ -184,14 +236,69 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
BorderPane borderPane = new BorderPane();
|
BorderPane borderPane = new BorderPane();
|
||||||
borderPane.setPadding(new Insets(5));
|
borderPane.setPadding(new Insets(5));
|
||||||
borderPane.setTop(search);
|
borderPane.setTop(topBar);
|
||||||
borderPane.setCenter(scrollPane);
|
borderPane.setCenter(scrollPane);
|
||||||
borderPane.setBottom(bottomPane);
|
borderPane.setBottom(bottomPane);
|
||||||
|
|
||||||
root.getChildren().add(borderPane);
|
root.getChildren().add(borderPane);
|
||||||
|
root.getChildren().add(popover);
|
||||||
setContent(root);
|
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() {
|
private void updateThumbSize() {
|
||||||
int width = Config.getInstance().getSettings().thumbWidth;
|
int width = Config.getInstance().getSettings().thumbWidth;
|
||||||
thumbWidth.getSelectionModel().select(Integer.valueOf(width));
|
thumbWidth.getSelectionModel().select(Integer.valueOf(width));
|
||||||
|
@ -242,7 +349,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
List<Model> models = updateService.getValue();
|
List<Model> models = updateService.getValue();
|
||||||
updateGrid(models);
|
updateGrid(models);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void updateGrid(List<? extends Model> 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<>();
|
Map<String, Object> event = new HashMap<>();
|
||||||
event.put("event", "tokens.sent");
|
event.put("event", "tokens.sent");
|
||||||
event.put("amount", tokens);
|
event.put("amount", tokens);
|
||||||
CamrecApplication.bus.post(event);
|
EventBusHolder.BUS.post(event);
|
||||||
} catch (Exception e1) {
|
} catch (Exception e1) {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
LOG.error("An error occured while sending tip", e1);
|
||||||
alert.setTitle("Error");
|
showError("Couldn't send tip", "An error occured while sending tip:", e1);
|
||||||
alert.setHeaderText("Couldn't send tip");
|
|
||||||
alert.setContentText("An error occured while sending tip: " + e1.getLocalizedMessage());
|
|
||||||
alert.showAndWait();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
showError("Couldn't send tip", "You entered an invalid amount of tokens", null);
|
||||||
alert.setTitle("Error");
|
|
||||||
alert.setHeaderText("Couldn't send tip");
|
|
||||||
alert.setContentText("You entered an invalid amount of tokens");
|
|
||||||
alert.showAndWait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -468,7 +567,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
translate.setFromX(0);
|
translate.setFromX(0);
|
||||||
translate.setFromY(0);
|
translate.setFromY(0);
|
||||||
translate.setByX(-tx.getTx() - 200);
|
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()));
|
StackPane.setMargin(iv, new Insets(offsetInViewPort, 0, 0, tx.getTx()));
|
||||||
translate.setInterpolator(Interpolator.EASE_BOTH);
|
translate.setInterpolator(Interpolator.EASE_BOTH);
|
||||||
FadeTransition fade = new FadeTransition(Duration.millis(duration), iv);
|
FadeTransition fade = new FadeTransition(Duration.millis(duration), iv);
|
||||||
|
@ -482,14 +583,42 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
pt.setOnFinished((evt) -> {
|
pt.setOnFinished((evt) -> {
|
||||||
root.getChildren().remove(iv);
|
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() {
|
private double getFollowedTabYPosition(Tab followedTab) {
|
||||||
TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider();
|
|
||||||
Tab followedTab = tabProvider.getFollowedTab();
|
|
||||||
TabPane tabPane = getTabPane();
|
TabPane tabPane = getTabPane();
|
||||||
int idx = tabPane.getTabs().indexOf(followedTab);
|
int idx = Math.max(0, tabPane.getTabs().indexOf(followedTab));
|
||||||
for (Node node : tabPane.getChildrenUnmodifiable()) {
|
for (Node node : tabPane.getChildrenUnmodifiable()) {
|
||||||
Parent p = (Parent) node;
|
Parent p = (Parent) node;
|
||||||
for (Node child : p.getChildrenUnmodifiable()) {
|
for (Node child : p.getChildrenUnmodifiable()) {
|
||||||
|
@ -633,6 +762,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
String[] tokens = filter.split(" ");
|
String[] tokens = filter.split(" ");
|
||||||
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
|
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
|
||||||
searchTextBuilder.append(' ');
|
searchTextBuilder.append(' ');
|
||||||
|
searchTextBuilder.append(m.getDisplayName());
|
||||||
|
searchTextBuilder.append(' ');
|
||||||
for (String tag : m.getTags()) {
|
for (String tag : m.getTags()) {
|
||||||
searchTextBuilder.append(tag).append(' ');
|
searchTextBuilder.append(tag).append(' ');
|
||||||
}
|
}
|
||||||
|
@ -652,7 +783,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
tokensMissing = true;
|
tokensMissing = true;
|
||||||
}
|
}
|
||||||
} else if(token.equals("public")) {
|
} else if(token.equals("public")) {
|
||||||
if(!m.getOnlineState(true).equals(token)) {
|
if(!m.getOnlineState(true).toString().equals(token)) {
|
||||||
tokensMissing = true;
|
tokensMissing = true;
|
||||||
}
|
}
|
||||||
} else if(!searchText.toLowerCase().contains(token.toLowerCase())) {
|
} else if(!searchText.toLowerCase().contains(token.toLowerCase())) {
|
||||||
|
@ -668,6 +799,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
public void setRecorder(Recorder recorder) {
|
public void setRecorder(Recorder recorder) {
|
||||||
this.recorder = recorder;
|
this.recorder = recorder;
|
||||||
|
popoverTreelist.setRecorder(recorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
|
@ -48,7 +47,7 @@ public class TipDialog extends TextInputDialog {
|
||||||
int tokens = get();
|
int tokens = get();
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (tokens <= 0) {
|
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.";
|
+ "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);
|
Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, ButtonType.NO, ButtonType.YES);
|
||||||
buyTokens.setTitle("No tokens");
|
buyTokens.setTitle("No tokens");
|
||||||
|
@ -56,7 +55,7 @@ public class TipDialog extends TextInputDialog {
|
||||||
buyTokens.showAndWait();
|
buyTokens.showAndWait();
|
||||||
TipDialog.this.close();
|
TipDialog.this.close();
|
||||||
if(buyTokens.getResult() == ButtonType.YES) {
|
if(buyTokens.getResult() == ButtonType.YES) {
|
||||||
DesktopIntegration.open(Chaturbate.AFFILIATE_LINK);
|
DesktopIntegration.open(site.getAffiliateLink());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getEditor().setDisable(false);
|
getEditor().setDisable(false);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
|
||||||
|
import ctbrec.event.EventBusHolder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
|
@ -24,7 +25,7 @@ public class TokenLabel extends Label {
|
||||||
public TokenLabel(Site site) {
|
public TokenLabel(Site site) {
|
||||||
this.site = site;
|
this.site = site;
|
||||||
setText("Tokens: loading…");
|
setText("Tokens: loading…");
|
||||||
CamrecApplication.bus.register(new Object() {
|
EventBusHolder.BUS.register(new Object() {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void tokensUpdates(Map<String, Object> e) {
|
public void tokensUpdates(Map<String, Object> e) {
|
||||||
if (Objects.equals("tokens", e.get("event"))) {
|
if (Objects.equals("tokens", e.get("event"))) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ctbrec.ui;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
import ctbrec.ui.CamrecApplication.Release;
|
import ctbrec.ui.CamrecApplication.Release;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
@ -36,6 +37,7 @@ public class UpdateTab extends Tab {
|
||||||
try {
|
try {
|
||||||
WebEngine webEngine = browser.getEngine();
|
WebEngine webEngine = browser.getEngine();
|
||||||
webEngine.load("https://raw.githubusercontent.com/0xboobface/ctbrec/master/CHANGELOG.md");
|
webEngine.load("https://raw.githubusercontent.com/0xboobface/ctbrec/master/CHANGELOG.md");
|
||||||
|
webEngine.setUserDataDirectory(Config.getInstance().getConfigDir());
|
||||||
vbox.getChildren().add(browser);
|
vbox.getChildren().add(browser);
|
||||||
VBox.setVgrow(browser, Priority.ALWAYS);
|
VBox.setVgrow(browser, Priority.ALWAYS);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
package ctbrec.ui;
|
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.control.Tab;
|
||||||
import javafx.scene.web.WebEngine;
|
import javafx.scene.web.WebEngine;
|
||||||
import javafx.scene.web.WebView;
|
import javafx.scene.web.WebView;
|
||||||
|
|
||||||
public class WebbrowserTab extends Tab {
|
public class WebbrowserTab extends Tab {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(WebbrowserTab.class);
|
||||||
|
|
||||||
public WebbrowserTab(String uri) {
|
public WebbrowserTab(String uri) {
|
||||||
WebView browser = new WebView();
|
WebView browser = new WebView();
|
||||||
WebEngine webEngine = browser.getEngine();
|
WebEngine webEngine = browser.getEngine();
|
||||||
|
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
|
||||||
|
webEngine.setJavaScriptEnabled(true);
|
||||||
webEngine.load(uri);
|
webEngine.load(uri);
|
||||||
setContent(browser);
|
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;
|
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 static ctbrec.Settings.ProxyType.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -51,18 +51,23 @@ public class ProxySettingsPane extends TitledPane implements EventHandler<Action
|
||||||
l = new Label("Host");
|
l = new Label("Host");
|
||||||
layout.add(l, 0, 1);
|
layout.add(l, 0, 1);
|
||||||
layout.add(proxyHost, 1, 1);
|
layout.add(proxyHost, 1, 1);
|
||||||
|
proxyHost.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||||
|
|
||||||
l = new Label("Port");
|
l = new Label("Port");
|
||||||
layout.add(l, 0, 2);
|
layout.add(l, 0, 2);
|
||||||
layout.add(proxyPort, 1, 2);
|
layout.add(proxyPort, 1, 2);
|
||||||
|
proxyPort.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||||
|
|
||||||
l = new Label("Username");
|
l = new Label("Username");
|
||||||
layout.add(l, 0, 3);
|
layout.add(l, 0, 3);
|
||||||
layout.add(proxyUser, 1, 3);
|
layout.add(proxyUser, 1, 3);
|
||||||
|
proxyUser.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||||
|
|
||||||
|
|
||||||
l = new Label("Password");
|
l = new Label("Password");
|
||||||
layout.add(l, 0, 4);
|
layout.add(l, 0, 4);
|
||||||
layout.add(proxyPassword, 1, 4);
|
layout.add(proxyPassword, 1, 4);
|
||||||
|
proxyPassword.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadConfig() {
|
private void loadConfig() {
|
||||||
|
@ -86,6 +91,7 @@ public class ProxySettingsPane extends TitledPane implements EventHandler<Action
|
||||||
public void handle(ActionEvent event) {
|
public void handle(ActionEvent event) {
|
||||||
setComponentDisableState();
|
setComponentDisableState();
|
||||||
settingsTab.showRestartRequired();
|
settingsTab.showRestartRequired();
|
||||||
|
settingsTab.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setComponentDisableState() {
|
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;
|
package ctbrec.ui.sites.bonga;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.sites.ConfigUI;
|
import ctbrec.Settings;
|
||||||
import ctbrec.sites.bonga.BongaCams;
|
import ctbrec.sites.bonga.BongaCams;
|
||||||
import ctbrec.ui.DesktopIntegration;
|
import ctbrec.ui.DesktopIntegration;
|
||||||
import ctbrec.ui.SettingsTab;
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
|
||||||
public class BongaCamsConfigUI implements ConfigUI {
|
public class BongaCamsConfigUI extends AbstractConfigUI {
|
||||||
|
|
||||||
private BongaCams bongaCams;
|
private BongaCams bongaCams;
|
||||||
|
|
||||||
public BongaCamsConfigUI(BongaCams bongaCams) {
|
public BongaCamsConfigUI(BongaCams bongaCams) {
|
||||||
|
@ -25,26 +26,56 @@ public class BongaCamsConfigUI implements ConfigUI {
|
||||||
@Override
|
@Override
|
||||||
public Parent createConfigPanel() {
|
public Parent createConfigPanel() {
|
||||||
GridPane layout = SettingsTab.createGridLayout();
|
GridPane layout = SettingsTab.createGridLayout();
|
||||||
layout.add(new Label("BongaCams User"), 0, 0);
|
Settings settings = Config.getInstance().getSettings();
|
||||||
TextField username = new TextField(Config.getInstance().getSettings().bongaUsername);
|
|
||||||
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaUsername = username.getText());
|
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.setFillWidth(username, true);
|
||||||
GridPane.setHgrow(username, Priority.ALWAYS);
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(username, 2);
|
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();
|
PasswordField password = new PasswordField();
|
||||||
password.setText(Config.getInstance().getSettings().bongaPassword);
|
password.setText(settings.bongaPassword);
|
||||||
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaPassword = password.getText());
|
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.setFillWidth(password, true);
|
||||||
GridPane.setHgrow(password, Priority.ALWAYS);
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(password, 2);
|
GridPane.setColumnSpan(password, 2);
|
||||||
layout.add(password, 1, 1);
|
layout.add(password, 1, row++);
|
||||||
|
|
||||||
Button createAccount = new Button("Create new Account");
|
Button createAccount = new Button("Create new Account");
|
||||||
createAccount.setOnAction((e) -> DesktopIntegration.open(bongaCams.getAffiliateLink()));
|
createAccount.setOnAction((e) -> DesktopIntegration.open(bongaCams.getAffiliateLink()));
|
||||||
layout.add(createAccount, 1, 2);
|
layout.add(createAccount, 1, row++);
|
||||||
GridPane.setColumnSpan(createAccount, 2);
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
GridPane.setMargin(password, 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
|
@Override
|
||||||
public boolean login() throws IOException {
|
public synchronized boolean login() throws IOException {
|
||||||
boolean automaticLogin = bongaCams.login();
|
boolean automaticLogin = bongaCams.login();
|
||||||
if(automaticLogin) {
|
if(automaticLogin) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package ctbrec.ui.sites.bonga;
|
package ctbrec.ui.sites.bonga;
|
||||||
|
|
||||||
|
import static ctbrec.Model.State.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -54,22 +56,41 @@ public class BongaCamsUpdateService extends PaginatedScheduledService {
|
||||||
JSONArray _models = json.getJSONArray("models");
|
JSONArray _models = json.getJSONArray("models");
|
||||||
for (int i = 0; i < _models.length(); i++) {
|
for (int i = 0; i < _models.length(); i++) {
|
||||||
JSONObject m = _models.getJSONObject(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);
|
BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name);
|
||||||
model.setUserId(m.getInt("user_id"));
|
|
||||||
boolean away = m.optBoolean("is_away");
|
boolean away = m.optBoolean("is_away");
|
||||||
boolean online = m.optBoolean("online") && !away;
|
boolean online = m.optBoolean("online");
|
||||||
model.setOnline(online);
|
model.setOnline(online);
|
||||||
|
|
||||||
if(online) {
|
if(online) {
|
||||||
|
model.setOnlineState(ONLINE);
|
||||||
if(away) {
|
if(away) {
|
||||||
model.setOnlineState("away");
|
model.setOnlineState(AWAY);
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
model.setOnlineState("offline");
|
model.setOnlineState(OFFLINE);
|
||||||
}
|
}
|
||||||
model.setPreview("https:" + m.getString("thumb_image"));
|
model.setPreview("https:" + m.getString("thumb_image"));
|
||||||
|
if(m.has("display_name")) {
|
||||||
|
model.setDisplayName(m.getString("display_name"));
|
||||||
|
}
|
||||||
models.add(model);
|
models.add(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,81 @@
|
||||||
package ctbrec.ui.sites.cam4;
|
package ctbrec.ui.sites.cam4;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.sites.ConfigUI;
|
import ctbrec.Settings;
|
||||||
import ctbrec.sites.cam4.Cam4;
|
import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.ui.DesktopIntegration;
|
import ctbrec.ui.DesktopIntegration;
|
||||||
import ctbrec.ui.SettingsTab;
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.Priority;
|
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
|
@Override
|
||||||
public Parent createConfigPanel() {
|
public Parent createConfigPanel() {
|
||||||
GridPane layout = SettingsTab.createGridLayout();
|
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);
|
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.setFillWidth(username, true);
|
||||||
GridPane.setHgrow(username, Priority.ALWAYS);
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(username, 2);
|
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();
|
PasswordField password = new PasswordField();
|
||||||
password.setText(Config.getInstance().getSettings().cam4Password);
|
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.setFillWidth(password, true);
|
||||||
GridPane.setHgrow(password, Priority.ALWAYS);
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(password, 2);
|
GridPane.setColumnSpan(password, 2);
|
||||||
layout.add(password, 1, 1);
|
layout.add(password, 1, row++);
|
||||||
|
|
||||||
Button createAccount = new Button("Create new Account");
|
Button createAccount = new Button("Create new Account");
|
||||||
createAccount.setOnAction((e) -> DesktopIntegration.open(Cam4.AFFILIATE_LINK));
|
createAccount.setOnAction((e) -> DesktopIntegration.open(Cam4.AFFILIATE_LINK));
|
||||||
layout.add(createAccount, 1, 2);
|
layout.add(createAccount, 1, row++);
|
||||||
GridPane.setColumnSpan(createAccount, 2);
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
GridPane.setMargin(password, 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);
|
String modelName = path.substring(1);
|
||||||
Cam4Model model = (Cam4Model) site.createModel(modelName);
|
Cam4Model model = (Cam4Model) site.createModel(modelName);
|
||||||
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis());
|
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis());
|
||||||
model.setOnlineState(parseOnlineState(cellHtml));
|
model.setOnlineStateByShowType(parseOnlineState(cellHtml));
|
||||||
models.add(model);
|
models.add(model);
|
||||||
}
|
}
|
||||||
return models.stream()
|
return models.stream()
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class Cam4SiteUi implements SiteUI {
|
||||||
public Cam4SiteUi(Cam4 cam4) {
|
public Cam4SiteUi(Cam4 cam4) {
|
||||||
this.cam4 = cam4;
|
this.cam4 = cam4;
|
||||||
tabProvider = new Cam4TabProvider(cam4);
|
tabProvider = new Cam4TabProvider(cam4);
|
||||||
configUI = new Cam4ConfigUI();
|
configUI = new Cam4ConfigUI(cam4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -44,7 +44,7 @@ public class Cam4SiteUi implements SiteUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean login() throws IOException {
|
public synchronized boolean login() throws IOException {
|
||||||
boolean automaticLogin = cam4.login();
|
boolean automaticLogin = cam4.login();
|
||||||
if(automaticLogin) {
|
if(automaticLogin) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
package ctbrec.ui.sites.camsoda;
|
package ctbrec.ui.sites.camsoda;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.sites.ConfigUI;
|
import ctbrec.Settings;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.ui.DesktopIntegration;
|
import ctbrec.ui.DesktopIntegration;
|
||||||
import ctbrec.ui.SettingsTab;
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
|
||||||
public class CamsodaConfigUI implements ConfigUI {
|
public class CamsodaConfigUI extends AbstractConfigUI {
|
||||||
|
|
||||||
private Camsoda camsoda;
|
private Camsoda camsoda;
|
||||||
|
|
||||||
public CamsodaConfigUI(Camsoda camsoda) {
|
public CamsodaConfigUI(Camsoda camsoda) {
|
||||||
|
@ -25,26 +26,56 @@ public class CamsodaConfigUI implements ConfigUI {
|
||||||
@Override
|
@Override
|
||||||
public Parent createConfigPanel() {
|
public Parent createConfigPanel() {
|
||||||
GridPane layout = SettingsTab.createGridLayout();
|
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);
|
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.setFillWidth(username, true);
|
||||||
GridPane.setHgrow(username, Priority.ALWAYS);
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(username, 2);
|
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();
|
PasswordField password = new PasswordField();
|
||||||
password.setText(Config.getInstance().getSettings().camsodaPassword);
|
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.setFillWidth(password, true);
|
||||||
GridPane.setHgrow(password, Priority.ALWAYS);
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(password, 2);
|
GridPane.setColumnSpan(password, 2);
|
||||||
layout.add(password, 1, 1);
|
layout.add(password, 1, row++);
|
||||||
|
|
||||||
Button createAccount = new Button("Create new Account");
|
Button createAccount = new Button("Create new Account");
|
||||||
createAccount.setOnAction((e) -> DesktopIntegration.open(camsoda.getAffiliateLink()));
|
createAccount.setOnAction((e) -> DesktopIntegration.open(camsoda.getAffiliateLink()));
|
||||||
layout.add(createAccount, 1, 2);
|
layout.add(createAccount, 1, row++);
|
||||||
GridPane.setColumnSpan(createAccount, 2);
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
GridPane.setMargin(password, 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;
|
package ctbrec.ui.sites.camsoda;
|
||||||
|
|
||||||
|
import static ctbrec.Model.State.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -47,7 +49,7 @@ public class CamsodaFollowedUpdateService extends PaginatedScheduledService {
|
||||||
JSONObject m = following.getJSONObject(i);
|
JSONObject m = following.getJSONObject(i);
|
||||||
CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname"));
|
CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname"));
|
||||||
boolean online = m.getInt("online") == 1;
|
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");
|
model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg");
|
||||||
models.add(model);
|
models.add(model);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ public class CamsodaSiteUi implements SiteUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean login() throws IOException {
|
public synchronized boolean login() throws IOException {
|
||||||
boolean automaticLogin = camsoda.login();
|
boolean automaticLogin = camsoda.login();
|
||||||
return automaticLogin;
|
return automaticLogin;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
@ -56,39 +58,43 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
JSONObject json = new JSONObject(response.body().string());
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
if(json.has("status") && json.getBoolean("status")) {
|
if(json.has("status") && json.getBoolean("status")) {
|
||||||
|
JSONArray template = json.getJSONArray("template");
|
||||||
JSONArray results = json.getJSONArray("results");
|
JSONArray results = json.getJSONArray("results");
|
||||||
for (int i = 0; i < results.length(); i++) {
|
for (int i = 0; i < results.length(); i++) {
|
||||||
JSONObject result = results.getJSONObject(i);
|
JSONObject result = results.getJSONObject(i);
|
||||||
if(result.has("tpl")) {
|
if(result.has("tpl")) {
|
||||||
JSONArray tpl = result.getJSONArray("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);
|
// int connections = tpl.getInt(2);
|
||||||
String streamName = tpl.getString(5);
|
String streamName = tpl.getString(getTemplateIndex(template, "stream_name"));
|
||||||
String tsize = tpl.getString(6);
|
String tsize = tpl.getString(getTemplateIndex(template, "tsize"));
|
||||||
String serverPrefix = tpl.getString(7);
|
String serverPrefix = tpl.getString(getTemplateIndex(template, "server_prefix"));
|
||||||
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
||||||
model.setDescription(tpl.getString(4));
|
model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html")));
|
||||||
model.setSortOrder(tpl.getFloat(3));
|
model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value")));
|
||||||
long unixtime = System.currentTimeMillis() / 1000;
|
long unixtime = System.currentTimeMillis() / 1000;
|
||||||
String preview = "https://thumbs-orig.camsoda.com/thumbs/"
|
String preview = "https://thumbs-orig.camsoda.com/thumbs/"
|
||||||
+ streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime;
|
+ streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime;
|
||||||
model.setPreview(preview);
|
model.setPreview(preview);
|
||||||
if(result.has("edge_servers")) {
|
JSONArray edgeServers = tpl.getJSONArray(getTemplateIndex(template, "edge_servers"));
|
||||||
JSONArray edgeServers = result.getJSONArray("edge_servers");
|
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
|
||||||
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
|
model.setDisplayName(displayName);
|
||||||
}
|
|
||||||
models.add(model);
|
models.add(model);
|
||||||
} else {
|
} else {
|
||||||
String name = result.getString("username");
|
String name = result.getString("username");
|
||||||
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
||||||
|
|
||||||
if(result.has("server_prefix")) {
|
if(result.has("server_prefix")) {
|
||||||
String serverPrefix = result.getString("server_prefix");
|
String serverPrefix = result.getString("server_prefix");
|
||||||
String streamName = result.getString("stream_name");
|
String streamName = result.getString("stream_name");
|
||||||
model.setSortOrder(result.getFloat("sort_value"));
|
model.setSortOrder(result.getFloat("sort_value"));
|
||||||
models.add(model);
|
models.add(model);
|
||||||
if(result.has("status")) {
|
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")) {
|
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;
|
package ctbrec.ui.sites.chaturbate;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.sites.ConfigUI;
|
import ctbrec.Settings;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.ui.DesktopIntegration;
|
import ctbrec.ui.DesktopIntegration;
|
||||||
import ctbrec.ui.SettingsTab;
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.Priority;
|
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
|
@Override
|
||||||
public Parent createConfigPanel() {
|
public Parent createConfigPanel() {
|
||||||
|
Settings settings = Config.getInstance().getSettings();
|
||||||
GridPane layout = SettingsTab.createGridLayout();
|
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);
|
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.setFillWidth(username, true);
|
||||||
GridPane.setHgrow(username, Priority.ALWAYS);
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(username, 2);
|
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();
|
PasswordField password = new PasswordField();
|
||||||
password.setText(Config.getInstance().getSettings().password);
|
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.setFillWidth(password, true);
|
||||||
GridPane.setHgrow(password, Priority.ALWAYS);
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(password, 2);
|
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");
|
Button createAccount = new Button("Create new Account");
|
||||||
createAccount.setOnAction((e) -> DesktopIntegration.open(Chaturbate.REGISTRATION_LINK));
|
createAccount.setOnAction((e) -> DesktopIntegration.open(Chaturbate.REGISTRATION_LINK));
|
||||||
layout.add(createAccount, 1, 2);
|
layout.add(createAccount, 1, row++);
|
||||||
GridPane.setColumnSpan(createAccount, 2);
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
GridPane.setMargin(password, 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));
|
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
|
||||||
|
username.setPrefWidth(300);
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ public class ChaturbateSiteUi implements SiteUI {
|
||||||
public ChaturbateSiteUi(Chaturbate chaturbate) {
|
public ChaturbateSiteUi(Chaturbate chaturbate) {
|
||||||
this.chaturbate = chaturbate;
|
this.chaturbate = chaturbate;
|
||||||
tabProvider = new ChaturbateTabProvider(chaturbate);
|
tabProvider = new ChaturbateTabProvider(chaturbate);
|
||||||
configUi = new ChaturbateConfigUi();
|
configUi = new ChaturbateConfigUi(chaturbate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -30,7 +30,7 @@ public class ChaturbateSiteUi implements SiteUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean login() throws IOException {
|
public synchronized boolean login() throws IOException {
|
||||||
return chaturbate.login();
|
return chaturbate.login();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package ctbrec.ui.sites.chaturbate;
|
package ctbrec.ui.sites.chaturbate;
|
||||||
|
|
||||||
import static ctbrec.sites.chaturbate.Chaturbate.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -21,17 +19,17 @@ public class ChaturbateTabProvider extends TabProvider {
|
||||||
public ChaturbateTabProvider(Chaturbate chaturbate) {
|
public ChaturbateTabProvider(Chaturbate chaturbate) {
|
||||||
this.chaturbate = chaturbate;
|
this.chaturbate = chaturbate;
|
||||||
this.recorder = chaturbate.getRecorder();
|
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
|
@Override
|
||||||
public List<Tab> getTabs(Scene scene) {
|
public List<Tab> getTabs(Scene scene) {
|
||||||
List<Tab> tabs = new ArrayList<>();
|
List<Tab> tabs = new ArrayList<>();
|
||||||
tabs.add(createTab("Featured", BASE_URI + "/"));
|
tabs.add(createTab("Featured", chaturbate.getBaseUrl() + "/"));
|
||||||
tabs.add(createTab("Female", BASE_URI + "/female-cams/"));
|
tabs.add(createTab("Female", chaturbate.getBaseUrl() + "/female-cams/"));
|
||||||
tabs.add(createTab("Male", BASE_URI + "/male-cams/"));
|
tabs.add(createTab("Male", chaturbate.getBaseUrl() + "/male-cams/"));
|
||||||
tabs.add(createTab("Couples", BASE_URI + "/couple-cams/"));
|
tabs.add(createTab("Couples", chaturbate.getBaseUrl() + "/couple-cams/"));
|
||||||
tabs.add(createTab("Trans", BASE_URI + "/trans-cams/"));
|
tabs.add(createTab("Trans", chaturbate.getBaseUrl() + "/trans-cams/"));
|
||||||
followedTab.setScene(scene);
|
followedTab.setScene(scene);
|
||||||
followedTab.setRecorder(recorder);
|
followedTab.setRecorder(recorder);
|
||||||
tabs.add(followedTab);
|
tabs.add(followedTab);
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
package ctbrec.ui.sites.myfreecams;
|
package ctbrec.ui.sites.myfreecams;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.sites.ConfigUI;
|
import ctbrec.Settings;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
import ctbrec.ui.DesktopIntegration;
|
import ctbrec.ui.DesktopIntegration;
|
||||||
import ctbrec.ui.SettingsTab;
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
|
||||||
public class MyFreeCamsConfigUI implements ConfigUI {
|
public class MyFreeCamsConfigUI extends AbstractConfigUI {
|
||||||
|
|
||||||
private MyFreeCams myFreeCams;
|
private MyFreeCams myFreeCams;
|
||||||
|
|
||||||
public MyFreeCamsConfigUI(MyFreeCams myFreeCams) {
|
public MyFreeCamsConfigUI(MyFreeCams myFreeCams) {
|
||||||
|
@ -24,32 +25,85 @@ public class MyFreeCamsConfigUI implements ConfigUI {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Parent createConfigPanel() {
|
public Parent createConfigPanel() {
|
||||||
|
int row = 0;
|
||||||
GridPane layout = SettingsTab.createGridLayout();
|
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);
|
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.setFillWidth(username, true);
|
||||||
GridPane.setHgrow(username, Priority.ALWAYS);
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(username, 2);
|
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();
|
PasswordField password = new PasswordField();
|
||||||
password.setText(Config.getInstance().getSettings().mfcPassword);
|
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.setFillWidth(password, true);
|
||||||
GridPane.setHgrow(password, Priority.ALWAYS);
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(password, 2);
|
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");
|
Button createAccount = new Button("Create new Account");
|
||||||
createAccount.setOnAction((e) -> DesktopIntegration.open(myFreeCams.getAffiliateLink()));
|
createAccount.setOnAction((e) -> DesktopIntegration.open(myFreeCams.getAffiliateLink()));
|
||||||
layout.add(createAccount, 1, 2);
|
layout.add(createAccount, 1, row);
|
||||||
GridPane.setColumnSpan(createAccount, 2);
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
GridPane.setMargin(password, 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));
|
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class MyFreeCamsSiteUi implements SiteUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean login() throws IOException {
|
public synchronized boolean login() throws IOException {
|
||||||
return myFreeCams.login();
|
return myFreeCams.login();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,8 @@ public class MyFreeCamsTabProvider extends TabProvider {
|
||||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10)));
|
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10)));
|
||||||
tabs.add(pop);
|
tabs.add(pop);
|
||||||
|
|
||||||
|
MyFreeCamsTableTab table = new MyFreeCamsTableTab(myFreeCams);
|
||||||
|
tabs.add(table);
|
||||||
|
|
||||||
return tabs;
|
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>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.10.0</version>
|
<version>1.15.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ctbrec;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import com.squareup.moshi.JsonReader;
|
import com.squareup.moshi.JsonReader;
|
||||||
|
@ -14,12 +15,14 @@ public abstract class AbstractModel implements Model {
|
||||||
|
|
||||||
private String url;
|
private String url;
|
||||||
private String name;
|
private String name;
|
||||||
|
private String displayName;
|
||||||
private String preview;
|
private String preview;
|
||||||
private String description;
|
private String description;
|
||||||
private List<String> tags = new ArrayList<>();
|
private List<String> tags = new ArrayList<>();
|
||||||
private int streamUrlIndex = -1;
|
private int streamUrlIndex = -1;
|
||||||
private boolean suspended = false;
|
private boolean suspended = false;
|
||||||
protected Site site;
|
protected Site site;
|
||||||
|
protected State onlineState = State.UNKNOWN;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||||
|
@ -46,6 +49,20 @@ public abstract class AbstractModel implements Model {
|
||||||
this.name = name;
|
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
|
@Override
|
||||||
public String getPreview() {
|
public String getPreview() {
|
||||||
return preview;
|
return preview;
|
||||||
|
@ -106,6 +123,15 @@ public abstract class AbstractModel implements Model {
|
||||||
this.suspended = suspended;
|
this.suspended = suspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
|
return onlineState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnlineState(State status) {
|
||||||
|
this.onlineState = status;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
final int prime = 31;
|
final int prime = 31;
|
||||||
|
@ -137,6 +163,13 @@ public abstract class AbstractModel implements Model {
|
||||||
return true;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getName();
|
return getName();
|
||||||
|
|
|
@ -7,6 +7,7 @@ import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -46,7 +47,6 @@ public class Config {
|
||||||
} else {
|
} else {
|
||||||
filename = "settings.json";
|
filename = "settings.json";
|
||||||
}
|
}
|
||||||
load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void load() throws FileNotFoundException, IOException {
|
private void load() throws FileNotFoundException, IOException {
|
||||||
|
@ -61,6 +61,13 @@ public class Config {
|
||||||
BufferedSource source = buffer.readFrom(fin);
|
BufferedSource source = buffer.readFrom(fin);
|
||||||
settings = adapter.fromJson(source);
|
settings = adapter.fromJson(source);
|
||||||
settings.httpTimeout = Math.max(settings.httpTimeout, 10_000);
|
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 {
|
} else {
|
||||||
LOG.error("Config file does not exist. Falling back to default values.");
|
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 {
|
public static synchronized void init(List<Site> sites) throws FileNotFoundException, IOException {
|
||||||
if(instance == null) {
|
if(instance == null) {
|
||||||
instance = new Config(sites);
|
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);
|
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");
|
return Objects.equals(System.getProperty("ctbrec.server.mode"), "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDevMode() {
|
||||||
|
return Objects.equals(System.getenv("CTBREC_DEV"), "1");
|
||||||
|
}
|
||||||
|
|
||||||
public File getConfigDir() {
|
public File getConfigDir() {
|
||||||
return configDir;
|
return configDir;
|
||||||
}
|
}
|
||||||
|
@ -113,10 +137,6 @@ public class Config {
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
|
||||||
String startTime = sdf.format(new Date());
|
String startTime = sdf.format(new Date());
|
||||||
File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts");
|
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;
|
return targetFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,33 +12,95 @@ import com.squareup.moshi.JsonWriter;
|
||||||
import ctbrec.recorder.download.StreamSource;
|
import ctbrec.recorder.download.StreamSource;
|
||||||
import ctbrec.sites.Site;
|
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 String getUrl();
|
||||||
|
|
||||||
public void setUrl(String url);
|
public void setUrl(String url);
|
||||||
|
|
||||||
|
public String getDisplayName();
|
||||||
|
|
||||||
|
public void setDisplayName(String name);
|
||||||
|
|
||||||
public String getName();
|
public String getName();
|
||||||
|
|
||||||
public void setName(String name);
|
public void setName(String name);
|
||||||
|
|
||||||
public String getPreview();
|
public String getPreview();
|
||||||
|
|
||||||
public void setPreview(String preview);
|
public void setPreview(String preview);
|
||||||
|
|
||||||
public List<String> getTags();
|
public List<String> getTags();
|
||||||
|
|
||||||
public void setTags(List<String> tags);
|
public void setTags(List<String> tags);
|
||||||
|
|
||||||
public String getDescription();
|
public String getDescription();
|
||||||
|
|
||||||
public void setDescription(String description);
|
public void setDescription(String description);
|
||||||
|
|
||||||
public int getStreamUrlIndex();
|
public int getStreamUrlIndex();
|
||||||
|
|
||||||
public void setStreamUrlIndex(int streamUrlIndex);
|
public void setStreamUrlIndex(int streamUrlIndex);
|
||||||
|
|
||||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException;
|
public boolean isOnline() throws IOException, ExecutionException, InterruptedException;
|
||||||
|
|
||||||
public boolean isOnline(boolean ignoreCache) 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 List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
|
||||||
|
|
||||||
public void invalidateCacheEntries();
|
public void invalidateCacheEntries();
|
||||||
|
|
||||||
public void receiveTip(int tokens) throws IOException;
|
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 int[] getStreamResolution(boolean failFast) throws ExecutionException;
|
||||||
|
|
||||||
public boolean follow() throws IOException;
|
public boolean follow() throws IOException;
|
||||||
|
|
||||||
public boolean unfollow() throws IOException;
|
public boolean unfollow() throws IOException;
|
||||||
|
|
||||||
public void setSite(Site site);
|
public void setSite(Site site);
|
||||||
|
|
||||||
public Site getSite();
|
public Site getSite();
|
||||||
|
|
||||||
public void writeSiteSpecificData(JsonWriter writer) throws IOException;
|
public void writeSiteSpecificData(JsonWriter writer) throws IOException;
|
||||||
|
|
||||||
public void readSiteSpecificData(JsonReader reader) throws IOException;
|
public void readSiteSpecificData(JsonReader reader) throws IOException;
|
||||||
|
|
||||||
public boolean isSuspended();
|
public boolean isSuspended();
|
||||||
|
|
||||||
public void setSuspended(boolean suspended);
|
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;
|
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.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.io.StreamRedirectThread;
|
||||||
|
|
||||||
public class OS {
|
public class OS {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(OS.class);
|
||||||
|
|
||||||
public static enum TYPE {
|
public static enum TYPE {
|
||||||
LINUX,
|
LINUX,
|
||||||
MAC,
|
MAC,
|
||||||
|
@ -72,4 +86,61 @@ public class OS {
|
||||||
}
|
}
|
||||||
return env;
|
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 Instant startDate;
|
||||||
private String path;
|
private String path;
|
||||||
private boolean hasPlaylist;
|
private boolean hasPlaylist;
|
||||||
private STATUS status;
|
private State status = State.UNKNOWN;
|
||||||
private int progress = -1;
|
private int progress = -1;
|
||||||
private long sizeInByte;
|
private long sizeInByte;
|
||||||
|
|
||||||
public static enum STATUS {
|
public static enum State {
|
||||||
RECORDING,
|
RECORDING("recording"),
|
||||||
GENERATING_PLAYLIST,
|
STOPPED("stopped"),
|
||||||
FINISHED,
|
GENERATING_PLAYLIST("generating playlist"),
|
||||||
DOWNLOADING,
|
POST_PROCESSING("post-processing"),
|
||||||
MERGING
|
FINISHED("finished"),
|
||||||
|
DOWNLOADING("downloading"),
|
||||||
|
UNKNOWN("unknown");
|
||||||
|
|
||||||
|
private String desc;
|
||||||
|
|
||||||
|
State(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recording() {}
|
public Recording() {}
|
||||||
|
@ -48,11 +61,11 @@ public class Recording {
|
||||||
this.startDate = startDate;
|
this.startDate = startDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public STATUS getStatus() {
|
public State getStatus() {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStatus(STATUS status) {
|
public void setStatus(State status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import ctbrec.event.EventHandlerConfiguration;
|
||||||
|
|
||||||
public class Settings {
|
public class Settings {
|
||||||
|
|
||||||
public enum ProxyType {
|
public enum ProxyType {
|
||||||
|
@ -30,6 +32,7 @@ public class Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean singlePlayer = true;
|
public boolean singlePlayer = true;
|
||||||
|
public boolean showPlayerStarting = false;
|
||||||
public boolean localRecording = true;
|
public boolean localRecording = true;
|
||||||
public int httpPort = 8080;
|
public int httpPort = 8080;
|
||||||
public int httpTimeout = 10000;
|
public int httpTimeout = 10000;
|
||||||
|
@ -37,22 +40,36 @@ public class Settings {
|
||||||
public String httpServer = "localhost";
|
public String httpServer = "localhost";
|
||||||
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
|
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
|
||||||
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
||||||
|
public long minimumSpaceLeftInBytes = 0;
|
||||||
|
public int minimumLengthInSeconds = 0;
|
||||||
public String mediaPlayer = "/usr/bin/mpv";
|
public String mediaPlayer = "/usr/bin/mpv";
|
||||||
public String postProcessing = "";
|
public String postProcessing = "";
|
||||||
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
||||||
public String password = ""; // chaturbate password 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 bongaUsername = "";
|
||||||
public String bongaPassword = "";
|
public String bongaPassword = "";
|
||||||
public String mfcUsername = "";
|
public String mfcUsername = "";
|
||||||
public String mfcPassword = "";
|
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 camsodaUsername = "";
|
||||||
public String camsodaPassword = "";
|
public String camsodaPassword = "";
|
||||||
public String cam4Username;
|
public String cam4Username = "";
|
||||||
public String cam4Password;
|
public String cam4Password = "";
|
||||||
|
public String streamateUsername = "";
|
||||||
|
public String streamatePassword = "";
|
||||||
public String lastDownloadDir = "";
|
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 determineResolution = false;
|
||||||
|
public boolean previewInThumbnails = true;
|
||||||
public boolean requireAuthentication = false;
|
public boolean requireAuthentication = false;
|
||||||
public boolean chooseStreamQuality = false;
|
public boolean chooseStreamQuality = false;
|
||||||
public int maximumResolution = 0;
|
public int maximumResolution = 0;
|
||||||
|
@ -62,7 +79,9 @@ public class Settings {
|
||||||
public String proxyPort;
|
public String proxyPort;
|
||||||
public String proxyUser;
|
public String proxyUser;
|
||||||
public String proxyPassword;
|
public String proxyPassword;
|
||||||
|
public String startTab = "Settings";
|
||||||
public int thumbWidth = 180;
|
public int thumbWidth = 180;
|
||||||
|
public boolean updateThumbnails = true;
|
||||||
public int windowWidth = 1340;
|
public int windowWidth = 1340;
|
||||||
public int windowHeight = 800;
|
public int windowHeight = 800;
|
||||||
public boolean windowMaximized = false;
|
public boolean windowMaximized = false;
|
||||||
|
@ -70,4 +89,13 @@ public class Settings {
|
||||||
public int windowY;
|
public int windowY;
|
||||||
public int splitRecordings = 0;
|
public int splitRecordings = 0;
|
||||||
public List<String> disabledSites = new ArrayList<>();
|
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;
|
package ctbrec;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
|
||||||
public class StringUtil {
|
public class StringUtil {
|
||||||
public static boolean isBlank(String s) {
|
public static boolean isBlank(String s) {
|
||||||
return s == null || s.trim().isEmpty();
|
return s == null || s.trim().isEmpty();
|
||||||
|
@ -8,4 +10,21 @@ public class StringUtil {
|
||||||
public static boolean isNotBlank(String s) {
|
public static boolean isNotBlank(String s) {
|
||||||
return !isBlank(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