diff --git a/client/pom.xml b/client/pom.xml
index a4f1ada7..1b16ac00 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -81,6 +81,10 @@
org.openjfx
javafx-media
+
+ org.openjfx
+ javafx-swing
+
org.eclipse.jetty
jetty-servlet
diff --git a/client/src/main/java/ctbrec/ui/action/AbstractPortraitAction.java b/client/src/main/java/ctbrec/ui/action/AbstractPortraitAction.java
new file mode 100644
index 00000000..b524a281
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/action/AbstractPortraitAction.java
@@ -0,0 +1,64 @@
+package ctbrec.ui.action;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import javax.imageio.ImageIO;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ctbrec.Config;
+import ctbrec.Model;
+import ctbrec.event.EventBusHolder;
+import javafx.scene.Node;
+
+public abstract class AbstractPortraitAction {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractPortraitAction.class);
+ public static final String FORMAT = "jpg";
+
+ protected Node source;
+ protected Model model;
+
+ protected BufferedImage convertToScaledJpg(BufferedImage original) {
+ java.awt.Image scaledPortrait = original.getScaledInstance(-1, 256, java.awt.Image.SCALE_SMOOTH);
+ BufferedImage bimage = new BufferedImage(scaledPortrait.getWidth(null), scaledPortrait.getHeight(null), BufferedImage.TYPE_INT_RGB);
+ Graphics2D bGr = bimage.createGraphics();
+ bGr.drawImage(scaledPortrait, 0, 0, null);
+ bGr.dispose();
+ return bimage;
+ }
+
+ protected boolean copyToCacheAsJpg(String portraitId, BufferedImage portrait) throws IOException {
+ File output = getPortraitFile(portraitId);
+ Files.createDirectories(output.getParentFile().toPath());
+ LOG.debug("Writing scaled portrait to {}", output);
+ return ImageIO.write(portrait, FORMAT, output);
+ }
+
+ protected File getPortraitFile(String portraitId) {
+ File configDir = Config.getInstance().getConfigDir();
+ File portraitDir = new File(configDir, "portraits");
+ File output = new File(portraitDir, portraitId + '.' + FORMAT);
+ return output;
+ }
+
+ protected void firePortraitChanged() {
+ EventBusHolder.BUS.post(new PortraitChangedEvent(model));
+ }
+
+ public static class PortraitChangedEvent {
+ private Model mdl;
+
+ public PortraitChangedEvent(Model model) {
+ this.mdl = model;
+ }
+
+ public Model getModel() {
+ return mdl;
+ }
+ }
+}
diff --git a/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java b/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java
index de3841e0..dce53769 100644
--- a/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java
+++ b/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java
@@ -1,7 +1,5 @@
package ctbrec.ui.action;
-import java.awt.Graphics2D;
-import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@@ -27,12 +25,9 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
-public class SetPortraitAction {
+public class SetPortraitAction extends AbstractPortraitAction {
private static final Logger LOG = LoggerFactory.getLogger(SetPortraitAction.class);
- public static final String FORMAT = "jpg";
- private Node source;
- private Model model;
private Consumer callback;
public SetPortraitAction(Node source, Model selectedModel, Consumer callback) {
@@ -69,6 +64,7 @@ public class SetPortraitAction {
Config.getInstance().getSettings().modelPortraits.put(model.getUrl(), portraitId);
try {
Config.getInstance().save();
+ firePortraitChanged();
runCallback();
} catch (IOException e) {
Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e);
@@ -104,7 +100,8 @@ public class SetPortraitAction {
private boolean processImageFile(String portraitId, String selectedFile) {
try {
- BufferedImage portrait = convertToScaledJpg(selectedFile);
+ BufferedImage original = ImageIO.read(new File(selectedFile));
+ BufferedImage portrait = convertToScaledJpg(original);
boolean success = copyToCacheAsJpg(portraitId, portrait);
if (!success) {
LOG.debug("Available formats: {}", Arrays.toString(ImageIO.getWriterFormatNames()));
@@ -117,28 +114,4 @@ public class SetPortraitAction {
return false;
}
}
-
- private BufferedImage convertToScaledJpg(String file) throws IOException {
- BufferedImage portrait = ImageIO.read(new File(file));
- Image scaledPortrait = portrait.getScaledInstance(-1, 256, Image.SCALE_SMOOTH);
- BufferedImage bimage = new BufferedImage(scaledPortrait.getWidth(null), scaledPortrait.getHeight(null), BufferedImage.TYPE_INT_RGB);
- Graphics2D bGr = bimage.createGraphics();
- bGr.drawImage(scaledPortrait, 0, 0, null);
- bGr.dispose();
- return bimage;
- }
-
- private boolean copyToCacheAsJpg(String portraitId, BufferedImage portrait) throws IOException {
- File output = getPortraitFile(portraitId);
- Files.createDirectories(output.getParentFile().toPath());
- LOG.debug("Writing scaled portrait to {}", output);
- return ImageIO.write(portrait, FORMAT, output);
- }
-
- private File getPortraitFile(String portraitId) {
- File configDir = Config.getInstance().getConfigDir();
- File portraitDir = new File(configDir, "portraits");
- File output = new File(portraitDir, portraitId + '.' + FORMAT);
- return output;
- }
}
diff --git a/client/src/main/java/ctbrec/ui/action/SetThumbAsPortraitAction.java b/client/src/main/java/ctbrec/ui/action/SetThumbAsPortraitAction.java
new file mode 100644
index 00000000..ec936331
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/action/SetThumbAsPortraitAction.java
@@ -0,0 +1,71 @@
+package ctbrec.ui.action;
+
+import java.awt.image.BufferedImage;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ctbrec.Config;
+import ctbrec.Model;
+import ctbrec.ui.controls.Dialogs;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+
+public class SetThumbAsPortraitAction extends AbstractPortraitAction {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SetThumbAsPortraitAction.class);
+
+ private Image image;
+
+ public SetThumbAsPortraitAction(Node source, Model model, Image image) {
+ this.source = source;
+ this.model = model;
+ this.image = image;
+ }
+
+ public void execute() {
+ source.setCursor(Cursor.WAIT);
+ try {
+ BufferedImage bufferedImage = convertFxImageToAwt(image);
+ BufferedImage croppedImage = cropImage(bufferedImage);
+ BufferedImage portrait = convertToScaledJpg(croppedImage);
+ String portraitId = UUID.nameUUIDFromBytes(model.getUrl().getBytes(StandardCharsets.UTF_8)).toString();
+ copyToCacheAsJpg(portraitId, portrait);
+ Config.getInstance().getSettings().modelPortraits.put(model.getUrl(), portraitId);
+ Config.getInstance().save();
+ firePortraitChanged();
+ } catch (Exception e) {
+ LOG.error("Error while changing portrait image", e);
+ Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e);
+ }
+ source.setCursor(Cursor.DEFAULT);
+ }
+
+ private BufferedImage cropImage(BufferedImage img) {
+ int w = img.getWidth();
+ int h = img.getHeight();
+ if (w > h) {
+ return cropSides(img);
+ } else {
+ return cropTopAndBottom(img);
+ }
+ }
+
+ private BufferedImage cropSides(BufferedImage img) {
+ int overlap = img.getWidth() - img.getHeight();
+ return img.getSubimage(overlap / 2, 0, img.getHeight(), img.getHeight());
+ }
+
+ private BufferedImage cropTopAndBottom(BufferedImage img) {
+ int overlap = img.getHeight() - img.getWidth();
+ return img.getSubimage(0, overlap/2, img.getWidth(), img.getWidth());
+ }
+
+ private BufferedImage convertFxImageToAwt(Image img) {
+ return SwingFXUtils.fromFXImage(img, null);
+ }
+}
diff --git a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java
index 694e2adf..9fa7c61b 100644
--- a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java
+++ b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java
@@ -144,7 +144,7 @@ public class ModelMenuContributor {
}
private void addPortrait(ContextMenu menu, List selectedModels) {
- var portrait = new MenuItem("Portrait");
+ var portrait = new MenuItem("Select Portrait");
portrait.setDisable(selectedModels.size() != 1);
portrait.setOnAction(e -> new SetPortraitAction(source, selectedModels.get(0), portraitCallback).execute());
menu.getItems().add(portrait);
diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java
index 2325332e..d24d3c20 100644
--- a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java
+++ b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java
@@ -33,6 +33,7 @@ import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TokenLabel;
+import ctbrec.ui.action.SetThumbAsPortraitAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
import ctbrec.ui.controls.SearchBox;
@@ -479,9 +480,13 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
.afterwards(() -> selectedModels.forEach(m -> getThumbCell(m).ifPresent(ThumbCell::update)))
.contributeToMenu(selectedModels, contextMenu);
+ var useImageAsPortrait = new MenuItem("Use As Portrait");
+ useImageAsPortrait.setOnAction(e -> new SetThumbAsPortraitAction(getTabPane(), cell.getModel(), cell.getImage()).execute());
+
var refresh = new MenuItem("Refresh Overview");
refresh.setOnAction(e -> refresh());
- contextMenu.getItems().addAll(refresh);
+
+ contextMenu.getItems().addAll(useImageAsPortrait, refresh);
var model = cell.getModel();
if (model instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
var debug = new MenuItem("debug");
diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java
index befa3ec8..129fb44f 100644
--- a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java
+++ b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java
@@ -20,15 +20,18 @@ import org.slf4j.LoggerFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
+import com.google.common.eventbus.Subscribe;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
+import ctbrec.event.EventBusHolder;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
+import ctbrec.ui.action.AbstractPortraitAction.PortraitChangedEvent;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.SetPortraitAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
@@ -107,6 +110,19 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
AbstractRecordedModelsTab(String text) {
super(text);
+ registerPortraitListener();
+ }
+
+ protected void registerPortraitListener() {
+ EventBusHolder.BUS.register(this);
+ }
+
+ @Subscribe
+ public void portraitChanged(PortraitChangedEvent e) {
+ portraitCache.invalidate(e.getModel());
+ if (table != null) {
+ table.refresh();
+ }
}
protected void createGui() {
diff --git a/master/pom.xml b/master/pom.xml
index 1bf9e70d..9d3055fb 100644
--- a/master/pom.xml
+++ b/master/pom.xml
@@ -107,6 +107,11 @@
javafx-media
${version.javafx}
+
+ org.openjfx
+ javafx-swing
+ ${version.javafx}
+
com.google.guava
guava