Add setting to show the number of active recordings in the tray icon

This commit is contained in:
0xb00bface 2022-04-09 20:00:56 +02:00
parent 737d1bbb55
commit 83cfee6568
6 changed files with 247 additions and 164 deletions

View File

@ -1,3 +1,7 @@
4.7.5
========================
* Add setting to show the number of active recordings in the tray
4.7.4
========================
* Fixed AmateurTV recordings

View File

@ -1,37 +1,9 @@
package ctbrec.ui;
import static ctbrec.event.Event.Type.*;
import java.awt.SplashScreen;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import ctbrec.sites.secretfriends.SecretFriends;
import ctbrec.sites.cherrytv.CherryTv;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
@ -54,11 +26,13 @@ import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.cherrytv.CherryTv;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.flirt4free.Flirt4Free;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.manyvids.MVLive;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.secretfriends.SecretFriends;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.stripchat.Stripchat;
@ -66,13 +40,7 @@ import ctbrec.sites.xlovecam.XloveCam;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.news.NewsTab;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.tabs.DonateTabFx;
import ctbrec.ui.tabs.HelpTab;
import ctbrec.ui.tabs.RecentlyWatchedTab;
import ctbrec.ui.tabs.RecordingsTab;
import ctbrec.ui.tabs.SiteTab;
import ctbrec.ui.tabs.TabSelectionListener;
import ctbrec.ui.tabs.UpdateTab;
import ctbrec.ui.tabs.*;
import ctbrec.ui.tabs.logging.LoggingTab;
import ctbrec.ui.tabs.recorded.RecordedTab;
import javafx.application.Application;
@ -81,11 +49,8 @@ import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.*;
import javafx.scene.control.TabPane.TabDragPolicy;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
@ -94,6 +59,22 @@ import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import okhttp3.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static ctbrec.event.Event.Type.*;
public class CamrecApplication extends Application {
@ -273,7 +254,7 @@ public class CamrecApplication extends Application {
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/tabs/ThumbCell.css");
primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue());
primaryStage.getScene().heightProperty()
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
primaryStage.setMaximized(Config.getInstance().getSettings().windowMaximized);
primaryStage.maximizedProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowMaximized = newVal);
Player.setScene(primaryStage.getScene());
@ -328,8 +309,8 @@ public class CamrecApplication extends Application {
private void suspendTabUpdates() {
tabPane.getTabs().stream()
.filter(TabSelectionListener.class::isInstance)
.forEach(t -> ((TabSelectionListener)t).deselected());
.filter(TabSelectionListener.class::isInstance)
.forEach(t -> ((TabSelectionListener) t).deselected());
}
private javafx.event.EventHandler<WindowEvent> createShutdownHandler() {
@ -433,6 +414,7 @@ public class CamrecApplication extends Application {
int modelCount = recorder.getModelCount();
List<Model> currentlyRecording = recorder.getCurrentlyRecording();
activeRecordings = currentlyRecording.size();
DesktopIntegration.updateTrayIcon(activeRecordings);
String windowTitle = getActiveRecordings(activeRecordings, modelCount) + title;
Platform.runLater(() -> primaryStage.setTitle(windowTitle));
updateStatus();

View File

@ -1,33 +1,8 @@
package ctbrec.ui;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS;
import ctbrec.event.EventBusHolder;
import ctbrec.io.StreamRedirector;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
@ -35,17 +10,25 @@ import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.TrayIcon.MessageType;
import java.io.File;
import java.io.IOException;
import java.net.URI;
public class DesktopIntegration {
private DesktopIntegration() {}
private DesktopIntegration() {
}
private static final Logger LOG = LoggerFactory.getLogger(DesktopIntegration.class);
private static SystemTray tray;
private static TrayIcon trayIcon;
private static Recorder recorder;
private static Stage primaryStage;
private static TrayIcon trayIcon;
public static void open(String uri) {
try {
@ -64,7 +47,7 @@ public class DesktopIntegration {
}
// try external helpers
var externalHelpers = new String[] { "kde-open5", "kde-open", "gnome-open", "xdg-open" };
var externalHelpers = new String[]{"kde-open5", "kde-open", "gnome-open", "xdg-open"};
var rt = Runtime.getRuntime();
for (String helper : externalHelpers) {
try {
@ -99,7 +82,7 @@ public class DesktopIntegration {
}
// try external helpers
var externalHelpers = new String[] { "kde-open5", "kde-open", "gnome-open", "xdg-open" };
var externalHelpers = new String[]{"kde-open5", "kde-open", "gnome-open", "xdg-open"};
var rt = Runtime.getRuntime();
for (String helper : externalHelpers) {
try {
@ -140,7 +123,7 @@ public class DesktopIntegration {
private static void notifyLinux(String title, String header, String msg) {
try {
Process p = Runtime.getRuntime().exec(new String[] {
Process p = Runtime.getRuntime().exec(new String[]{
"notify-send",
"-u", "normal",
"-t", "5000",
@ -185,91 +168,18 @@ public class DesktopIntegration {
private static boolean createTrayIcon(Stage stage) {
if (SystemTray.isSupported()) {
if (tray == null) {
String title = CamrecApplication.title;
tray = SystemTray.getSystemTray();
var image = Toolkit.getDefaultToolkit().createImage(DesktopIntegration.class.getResource("/icon64.png"));
PopupMenu menu = createTrayContextMenu(stage);
trayIcon = new TrayIcon(image, title, menu);
trayIcon.setImageAutoSize(true);
trayIcon.setToolTip(title);
try {
tray.add(trayIcon);
} catch (AWTException e) {
LOG.error("Couldn't add tray icon", e);
}
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
toggleVisibility(stage);
}
}
});
boolean created = false;
if (trayIcon == null) {
trayIcon = new ctbrec.ui.TrayIcon(stage, recorder);
created = trayIcon.createTrayIcon();
}
return true;
return created;
} else {
LOG.error("SystemTray notifications not supported by this OS");
return false;
}
}
private static PopupMenu createTrayContextMenu(Stage stage) {
var menu = new PopupMenu();
var show = new MenuItem("Show");
show.addActionListener(evt -> restoreStage(stage));
menu.add(show);
menu.addSeparator();
var pauseRecording = new MenuItem("Pause recording");
pauseRecording.addActionListener(evt -> {
try {
recorder.pause();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(stage.getScene(), "Pausing recording", "Pausing of the recorder failed", e);
}
});
menu.add(pauseRecording);
var resumeRecording = new MenuItem("Resume recording");
resumeRecording.addActionListener(evt -> {
try {
recorder.resume();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(stage.getScene(), "Resuming recording", "Resuming of the recorder failed", e);
}
});
menu.add(resumeRecording);
menu.addSeparator();
var exit = new MenuItem("Exit");
exit.addActionListener(evt -> exit());
menu.add(exit);
return menu;
}
private static void toggleVisibility(Stage stage) {
if (stage.isShowing()) {
Platform.runLater(stage::hide);
} else {
restoreStage(stage);
}
}
private static void restoreStage(Stage stage) {
Platform.runLater(() -> {
stage.setX(Config.getInstance().getSettings().windowX);
stage.setY(Config.getInstance().getSettings().windowY);
LOG.debug("Restoring window location {},{}", stage.getX(), stage.getY());
stage.setIconified(false);
stage.show();
stage.toFront();
EventBusHolder.BUS.post(Map.of("event", "stage_restored"));
});
}
private static void exit() {
EventBusHolder.BUS.post(Map.of("event", "shutdown"));
}
public static void setRecorder(Recorder recorder) {
DesktopIntegration.recorder = recorder;
}
@ -277,4 +187,10 @@ public class DesktopIntegration {
public static void setPrimaryStage(Stage primaryStage) {
DesktopIntegration.primaryStage = primaryStage;
}
public static void updateTrayIcon(int activeRecordings) {
if (trayIcon != null) {
trayIcon.updateActiveRecordings(activeRecordings);
}
}
}

View File

@ -0,0 +1,179 @@
package ctbrec.ui;
import ctbrec.Config;
import ctbrec.event.EventBusHolder;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import static java.awt.Font.BOLD;
import static java.awt.RenderingHints.KEY_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
public class TrayIcon {
private static final Logger LOG = LoggerFactory.getLogger(TrayIcon.class);
private final Stage stage;
private final Recorder recorder;
private SystemTray tray;
private java.awt.TrayIcon awtTrayIcon;
private BufferedImage background;
public TrayIcon(Stage stage, Recorder recorder) {
this.stage = stage;
this.recorder = recorder;
}
boolean createTrayIcon() {
if (SystemTray.isSupported()) {
if (tray == null) {
String title = CamrecApplication.title;
tray = SystemTray.getSystemTray();
BufferedImage image = null;
try {
image = createImage(recorder.getCurrentlyRecording().size());
} catch (Exception e) {
// fail silently
}
PopupMenu menu = createTrayContextMenu(stage);
awtTrayIcon = new java.awt.TrayIcon(image, title, menu);
awtTrayIcon.setImageAutoSize(true);
awtTrayIcon.setToolTip(title);
try {
tray.add(awtTrayIcon);
} catch (AWTException e) {
LOG.error("Couldn't add tray icon", e);
}
awtTrayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
toggleVisibility(stage);
}
}
});
}
return true;
} else {
LOG.error("SystemTray notifications not supported by this OS");
return false;
}
}
private PopupMenu createTrayContextMenu(Stage stage) {
var menu = new PopupMenu();
var show = new MenuItem("Show");
show.addActionListener(evt -> restoreStage(stage));
menu.add(show);
menu.addSeparator();
var pauseRecording = new MenuItem("Pause recording");
pauseRecording.addActionListener(evt -> {
try {
recorder.pause();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(stage.getScene(), "Pausing recording", "Pausing of the recorder failed", e);
}
});
menu.add(pauseRecording);
var resumeRecording = new MenuItem("Resume recording");
resumeRecording.addActionListener(evt -> {
try {
recorder.resume();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(stage.getScene(), "Resuming recording", "Resuming of the recorder failed", e);
}
});
menu.add(resumeRecording);
menu.addSeparator();
var exit = new MenuItem("Exit");
exit.addActionListener(evt -> exit());
menu.add(exit);
return menu;
}
private void restoreStage(Stage stage) {
Platform.runLater(() -> {
stage.setX(Config.getInstance().getSettings().windowX);
stage.setY(Config.getInstance().getSettings().windowY);
LOG.debug("Restoring window location {},{}", stage.getX(), stage.getY());
stage.setIconified(false);
stage.show();
stage.toFront();
EventBusHolder.BUS.post(Map.of("event", "stage_restored"));
});
}
private void toggleVisibility(Stage stage) {
if (stage.isShowing()) {
Platform.runLater(stage::hide);
} else {
restoreStage(stage);
}
}
private void exit() {
EventBusHolder.BUS.post(Map.of("event", "shutdown"));
}
public void displayMessage(String header, String msg, java.awt.TrayIcon.MessageType info) {
createTrayIcon();
awtTrayIcon.displayMessage(header, msg, info);
}
public void updateActiveRecordings(int activeRecordings) {
try {
createTrayIcon();
awtTrayIcon.setImage(createImage(activeRecordings));
} catch (IOException e) {
LOG.error("Couldn't update tray icon image", e);
}
}
private BufferedImage createImage(int number) throws IOException {
if (this.background == null) {
this.background = ImageIO.read(TrayIcon.class.getResource("/icon64.png"));
}
BufferedImage image = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.drawImage(background, 0, 0, (img, infoflags, x, y, width, height) -> false);
if (number > 0 && Config.getInstance().getSettings().showActiveRecordingsInTray) {
g2.setColor(Color.decode("#dc4444"));
g2.fillOval(0, 0, 64, 64);
String text = String.valueOf(number);
String fontFamily = Config.getInstance().getSettings().showActiveRecordingsInTrayFont;
int fontSize = Config.getInstance().getSettings().showActiveRecordingsInTrayFontSize;
g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
Font font = new Font(fontFamily, BOLD, fontSize);
g2.setFont(font);
FontMetrics fontMetrics = g2.getFontMetrics(font);
LineMetrics lineMetrics = fontMetrics.getLineMetrics(text, g2);
Rectangle2D stringBounds = fontMetrics.getStringBounds(text, g2);
g2.setColor(Color.decode(Config.getInstance().getSettings().showActiveRecordingsInTrayColor));
int x = (int) (image.getWidth() - stringBounds.getWidth()) / 2;
int y = (int) (((image.getHeight() - lineMetrics.getAscent()) / 2) - 8 - stringBounds.getY());
g2.drawString(text, x, y);
g2.dispose();
}
return image;
}
}

View File

@ -103,6 +103,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleBooleanProperty recordedModelsPerSite;
private SimpleBooleanProperty requireAuthentication;
private SimpleBooleanProperty totalModelCountInTitle;
private SimpleBooleanProperty showActiveRecordingsInTray;
private SimpleBooleanProperty transportLayerSecurity;
private SimpleBooleanProperty fastScrollSpeed;
private SimpleBooleanProperty useHlsdl;
@ -174,6 +175,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication);
requireAuthentication.addListener(this::requireAuthenticationChanged);
totalModelCountInTitle = new SimpleBooleanProperty(null, "totalModelCountInTitle", settings.totalModelCountInTitle);
showActiveRecordingsInTray = new SimpleBooleanProperty(null, "showActiveRecordingsInTray", settings.showActiveRecordingsInTray);
transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity);
recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote");
postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
@ -200,7 +202,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
List<Category> siteCategories = new ArrayList<>();
for (Site site : sites) {
ofNullable(SiteUiFactory.getUi(site)).map(SiteUI::getConfigUI).map(ConfigUI::createConfigPanel)
.ifPresent(configPanel -> siteCategories.add(Category.of(site.getName(), configPanel)));
.ifPresent(configPanel -> siteCategories.add(Category.of(site.getName(), configPanel)));
}
var storage = new CtbrecPreferencesStorage(config);
@ -234,6 +236,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Date format (empty = system default)", dateTimeFormat, DATE_FORMATTER_TOOLTIP).needsRestart(),
Setting.of("Display stream resolution in overview", determineResolution),
Setting.of("Total model count in title", totalModelCountInTitle, "Show the total number of models in the title bar"),
Setting.of("Show active recordings counter in tray", showActiveRecordingsInTray, "Show the number of running recorings in the tray icon"),
Setting.of("Show grid lines in tables", showGridLinesInTables, "Show grid lines in tables").needsRestart(),
Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart())),
Category.of("Recorder",
@ -255,7 +258,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Group.of("Timeout",
Setting.of("Don't record from", timeoutRecordingStartingAt),
Setting.of("Until", timeoutRecordingEndingAt)
),
),
Group.of("Location",
Setting.of("Record Location", recordLocal).needsRestart(),
Setting.of("Server", server),
@ -302,7 +305,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
restartNotification.setOpacity(0);
restartNotification.setStyle("-fx-font-size: 28; -fx-padding: .3em");
restartNotification
.setBorder(new Border(new BorderStroke(Color.web(settings.colorAccent), BorderStrokeStyle.SOLID, new CornerRadii(5), new BorderWidths(2))));
.setBorder(new Border(new BorderStroke(Color.web(settings.colorAccent), BorderStrokeStyle.SOLID, new CornerRadii(5), new BorderWidths(2))));
restartNotification.setBackground(new Background(new BackgroundFill(Color.web(settings.colorBase), new CornerRadii(5), Insets.EMPTY)));
stackPane.getChildren().add(restartNotification);
StackPane.setAlignment(restartNotification, Pos.TOP_RIGHT);

View File

@ -1,18 +1,12 @@
package ctbrec;
import java.io.File;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.recorder.postprocessing.PostProcessor;
import java.io.File;
import java.time.LocalTime;
import java.util.*;
public class Settings {
public enum DirectoryStructure {
@ -21,6 +15,7 @@ public class Settings {
ONE_PER_RECORDING("one directory for each recording");
private final String description;
DirectoryStructure(String description) {
this.description = description;
}
@ -169,6 +164,10 @@ public class Settings {
public int segmentErrorMeasurePeriodInSecs = 20;
public int segmentErrorThresholdToStopRecording = 5;
public String servletContext = "";
public boolean showActiveRecordingsInTray = true;
public String showActiveRecordingsInTrayColor = "#FFFFFF";
public String showActiveRecordingsInTrayFont = "SansSerif";
public int showActiveRecordingsInTrayFontSize = 48;
public boolean showGridLinesInTables = true;
public boolean showPlayerStarting = false;
public String showupUsername = "";