Add mechanisms to have credentials/settings for each site

Each site can now provide its own configuration panel. For
Chaturbate and MyFreeCams these panels contain input fields
for the user credentials.
This commit is contained in:
0xboobface 2018-10-25 12:27:46 +02:00
parent 7e6f5bbc2c
commit f0154857d9
11 changed files with 222 additions and 142 deletions

View File

@ -20,8 +20,10 @@ public class Settings {
public String httpServer = "localhost";
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
public String mediaPlayer = "/usr/bin/mpv";
public String username = "";
public String password = "";
public String username = ""; // chaturbate username TODO maybe rename this onetime
public String password = ""; // chaturbate password TODO maybe rename this onetime
public String mfcUsername = "";
public String mfcPassword = "";
public String lastDownloadDir = "";
public List<Model> models = new ArrayList<Model>();

View File

@ -6,6 +6,7 @@ import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder;
import ctbrec.ui.TabProvider;
import javafx.scene.Node;
public interface Site {
public String getName();
@ -23,4 +24,6 @@ public interface Site {
public boolean supportsTips();
public boolean supportsFollow();
public boolean isSiteForModel(Model m);
public Node getConfigurationGui();
public boolean credentialsAvailable();
}

View File

@ -30,8 +30,18 @@ import ctbrec.Settings;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.HtmlParser;
import ctbrec.ui.SettingsTab;
import ctbrec.ui.TabProvider;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
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;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
@ -306,4 +316,40 @@ public class Chaturbate implements Site {
response.close();
}
}
@Override
public Node getConfigurationGui() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("Chaturbate User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().username);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().username = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("Chaturbate Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().password);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().password = password.getText());
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntergation.open(Chaturbate.REGISTRATION_LINK));
layout.add(createAccount, 1, 2);
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;
}
@Override
public boolean credentialsAvailable() {
String username = Config.getInstance().getSettings().username;
return username != null && !username.trim().isEmpty();
}
}

View File

@ -38,7 +38,7 @@ public class FriendsUpdateService extends PaginatedScheduledService {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
if(StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) {
if(StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().mfcUsername)) {
return Collections.emptyList();
} else {
List<MyFreeCamsModel> models = new ArrayList<>();

View File

@ -4,11 +4,22 @@ import java.io.IOException;
import org.jsoup.select.Elements;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.HtmlParser;
import ctbrec.ui.SettingsTab;
import ctbrec.ui.TabProvider;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
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;
import okhttp3.Request;
import okhttp3.Response;
@ -116,4 +127,41 @@ public class MyFreeCams implements Site {
public MyFreeCamsClient getClient() {
return client;
}
@Override
public Node getConfigurationGui() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("MyFreeCams User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().mfcUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcUsername = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("MyFreeCams Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().mfcPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcPassword = password.getText());
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntergation.open(BASE_URI + "/php/signup.php?request=register"));
layout.add(createAccount, 1, 2);
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;
}
@Override
public boolean credentialsAvailable() {
String username = Config.getInstance().getSettings().mfcUsername;
return username != null && !username.trim().isEmpty();
}
}

View File

@ -216,7 +216,7 @@ public class MyFreeCamsClient {
case EXTDATA:
if(message.getArg1() == MessageTypes.LOGIN) {
chatToken = message.getMessage();
String username = Config.getInstance().getSettings().username;
String username = Config.getInstance().getSettings().mfcUsername;
if(StringUtil.isNotBlank(username)) {
boolean login = mfc.getHttpClient().login();
if (login) {

View File

@ -30,8 +30,8 @@ public class MyFreeCamsHttpClient extends HttpClient {
return true;
}
String username = Config.getInstance().getSettings().username;
String password = Config.getInstance().getSettings().password;
String username = Config.getInstance().getSettings().mfcUsername;
String password = Config.getInstance().getSettings().mfcPassword;
RequestBody body = new FormBody.Builder()
.add("username", username)
.add("password", password)

View File

@ -100,7 +100,7 @@ public class CamrecApplication extends Application {
rootPane.getTabs().add(modelsTab);
RecordingsTab recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
rootPane.getTabs().add(recordingsTab);
settingsTab = new SettingsTab();
settingsTab = new SettingsTab(sites);
rootPane.getTabs().add(settingsTab);
rootPane.getTabs().add(new DonateTabFx());

View File

@ -13,11 +13,10 @@ import com.sun.javafx.collections.ObservableListWrapper;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.Site;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
@ -25,7 +24,6 @@ import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
@ -40,6 +38,7 @@ import javafx.scene.layout.BorderWidths;
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.stage.DirectoryChooser;
import javafx.stage.FileChooser;;
@ -48,29 +47,29 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
private static final int CHECKBOX_MARGIN = 6;
public static final int CHECKBOX_MARGIN = 6;
private TextField recordingsDirectory;
private Button recordingsDirectoryButton;
private TextField mediaPlayer;
private TextField username;
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 PasswordField password;
private RadioButton recordLocal;
private RadioButton recordRemote;
private ToggleGroup recordLocation;
private ProxySettingsPane proxySettingsPane;
private TitledPane ctb;
private ComboBox<SplitAfterOption> splitAfter;
private List<Site> sites;
public SettingsTab() {
public SettingsTab(List<Site> sites) {
this.sites = sites;
setText("Settings");
createGui();
setClosable(false);
setRecordingMode(recordLocal.isSelected());
}
private void createGui() {
@ -80,127 +79,34 @@ public class SettingsTab extends Tab implements TabSelectionListener {
mainLayout.setPadding(new Insets(15));
setContent(mainLayout);
GridPane layout = createGridLayout();
Label l = new Label("Display stream resolution in overview");
layout.add(l, 0, 0);
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, 0);
l = new Label("Manually select stream quality");
layout.add(l, 0, 1);
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, 1);
l = new Label("Split recordings after (minutes)");
layout.add(l, 0, 2);
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<>(new ObservableListWrapper<>(options));
layout.add(splitAfter, 1, 2);
setSplitAfterValue();
splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue());
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(splitAfter, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
TitledPane general = new TitledPane("General", layout);
general.setCollapsible(false);
mainLayout.add(general, 0, 0);
layout = createGridLayout();
layout.add(new Label("Recordings Directory"), 0, 0);
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);
layout.add(recordingsDirectory, 1, 0);
recordingsDirectoryButton = createRecordingsBrowseButton();
layout.add(recordingsDirectoryButton, 3, 0);
layout.add(new Label("Player"), 0, 1);
mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer);
mediaPlayer.focusedProperty().addListener(createMpvFocusListener());
GridPane.setFillWidth(mediaPlayer, true);
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
GridPane.setColumnSpan(mediaPlayer, 2);
layout.add(mediaPlayer, 1, 1);
layout.add(createMpvBrowseButton(), 3, 1);
l = new Label("Allow multiple players");
layout.add(l, 0, 2);
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(multiplePlayers, new Insets(3, 0, 0, CHECKBOX_MARGIN));
layout.add(multiplePlayers, 1, 2);
TitledPane locations = new TitledPane("Locations", layout);
locations.setCollapsible(false);
mainLayout.add(locations, 0, 1);
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, 0);
mainLayout.add(rightSide, 1, 0);
leftSide.getChildren().add(createGeneralPanel());
leftSide.getChildren().add(createLocationsPanel());
leftSide.getChildren().add(createRecordLocationPanel());
proxySettingsPane = new ProxySettingsPane();
mainLayout.add(proxySettingsPane, 1, 0);
GridPane.setRowSpan(proxySettingsPane, 2);
GridPane.setFillWidth(proxySettingsPane, true);
GridPane.setHgrow(proxySettingsPane, Priority.ALWAYS);
GridPane.setValignment(proxySettingsPane, VPos.TOP);
leftSide.getChildren().add(proxySettingsPane);
layout = createGridLayout();
layout.add(new Label("Chaturbate User"), 0, 0);
username = new TextField(Config.getInstance().getSettings().username);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().username = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("Chaturbate Password"), 0, 1);
password = new PasswordField();
password.setText(Config.getInstance().getSettings().password);
password.focusedProperty().addListener((e) -> {
if(!password.getText().isEmpty()) {
Config.getInstance().getSettings().password = password.getText();
for (Site site : sites) {
Node siteConfig = site.getConfigurationGui();
if(siteConfig != null) {
TitledPane pane = new TitledPane(site.getName(), siteConfig);
pane.setCollapsible(false);
rightSide.getChildren().add(pane);
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
}
}
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntergation.open(Chaturbate.REGISTRATION_LINK));
layout.add(createAccount, 1, 2);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, CHECKBOX_MARGIN));
ctb = new TitledPane("Chaturbate", layout);
ctb.setCollapsible(false);
mainLayout.add(ctb, 0, 2);
layout = createGridLayout();
l = new Label("Record Location");
private Node createRecordLocationPanel() {
GridPane layout = createGridLayout();
Label l = new Label("Record Location");
layout.add(l, 0, 0);
recordLocation = new ToggleGroup();
recordLocal = new RadioButton("Local");
@ -280,9 +186,89 @@ public class SettingsTab extends Tab implements TabSelectionListener {
TitledPane recordLocation = new TitledPane("Record Location", layout);
recordLocation.setCollapsible(false);
mainLayout.add(recordLocation, 0, 3);
return recordLocation;
}
setRecordingMode(recordLocal.isSelected());
private Node createLocationsPanel() {
GridPane layout = createGridLayout();
layout.add(new Label("Recordings Directory"), 0, 0);
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);
layout.add(recordingsDirectory, 1, 0);
recordingsDirectoryButton = createRecordingsBrowseButton();
layout.add(recordingsDirectoryButton, 3, 0);
layout.add(new Label("Player"), 0, 1);
mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer);
mediaPlayer.focusedProperty().addListener(createMpvFocusListener());
GridPane.setFillWidth(mediaPlayer, true);
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
GridPane.setColumnSpan(mediaPlayer, 2);
layout.add(mediaPlayer, 1, 1);
layout.add(createMpvBrowseButton(), 3, 1);
Label l = new Label("Allow multiple players");
layout.add(l, 0, 2);
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(multiplePlayers, new Insets(3, 0, 0, CHECKBOX_MARGIN));
layout.add(multiplePlayers, 1, 2);
TitledPane locations = new TitledPane("Locations", layout);
locations.setCollapsible(false);
return locations;
}
private Node createGeneralPanel() {
GridPane layout = createGridLayout();
Label l = new Label("Display stream resolution in overview");
layout.add(l, 0, 0);
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, 0);
l = new Label("Manually select stream quality");
layout.add(l, 0, 1);
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, 1);
l = new Label("Split recordings after (minutes)");
layout.add(l, 0, 2);
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<>(new ObservableListWrapper<>(options));
layout.add(splitAfter, 1, 2);
setSplitAfterValue();
splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue());
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(splitAfter, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
TitledPane general = new TitledPane("General", layout);
general.setCollapsible(false);
return general;
}
private void setSplitAfterValue() {
@ -302,7 +288,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
restart.show();
}
static GridPane createGridLayout() {
public static GridPane createGridLayout() {
GridPane layout = new GridPane();
layout.setPadding(new Insets(10));
layout.setHgap(5);

View File

@ -1,6 +1,5 @@
package ctbrec.ui;
import ctbrec.Config;
import ctbrec.sites.Site;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -24,9 +23,7 @@ public class SiteTab extends Tab implements TabSelectionListener {
siteTabPane = new SiteTabPane(site, scene);
rootPane.setCenter(siteTabPane);
String username = Config.getInstance().getSettings().username;
if (site.supportsTips() && username != null && !username.trim().isEmpty()) {
if (site.supportsTips() && site.credentialsAvailable()) {
Button buyTokens = new Button("Buy Tokens");
buyTokens.setOnAction((e) -> DesktopIntergation.open(site.getBuyTokensLink()));
TokenLabel tokenBalance = new TokenLabel(site);

View File

@ -367,8 +367,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
}
}
});
String username = Config.getInstance().getSettings().username;
sendTip.setDisable(username == null || username.trim().isEmpty());
sendTip.setDisable(!site.credentialsAvailable());
// check, if other cells are selected, too. in that case, we have to disable menu item, which make sense only for
// single selections. but only do that, if the popup has been triggered on a selected cell. otherwise remove the
@ -385,7 +384,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
}
}
ContextMenu contextMenu = new ContextMenu();
contextMenu.setAutoHide(true);
contextMenu.setHideOnEscape(true);
@ -393,7 +391,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
contextMenu.getItems().addAll(openInPlayer, startStop);
if(site.supportsFollow()) {
MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
followOrUnFollow.setDisable(username == null || username.trim().isEmpty());
followOrUnFollow.setDisable(!site.credentialsAvailable());
contextMenu.getItems().add(followOrUnFollow);
}
if(site.supportsTips()) {