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