From 28ca1932e921aadaa764f6fecdfe96671089fe70 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 15 Aug 2021 14:12:47 +0200 Subject: [PATCH] Add first working client side version for model portraits --- .../ctbrec/ui/action/SetPortraitAction.java | 108 ++++++++++++++++++ .../ui/controls/AbstractFileSelectionBox.java | 1 + .../ctbrec/ui/menu/ModelMenuContributor.java | 17 +++ .../ui/tabs/recorded/ImageTableCell.java | 31 +++++ .../ui/tabs/recorded/RecordedModelsTab.java | 53 ++++++++- client/src/main/resources/silhouette_256.png | Bin 0 -> 9373 bytes .../src/main/java/ctbrec/AbstractModel.java | 2 +- common/src/main/java/ctbrec/Settings.java | 3 +- 8 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/action/SetPortraitAction.java create mode 100644 client/src/main/java/ctbrec/ui/tabs/recorded/ImageTableCell.java create mode 100644 client/src/main/resources/silhouette_256.png diff --git a/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java b/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java new file mode 100644 index 00000000..c543da32 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java @@ -0,0 +1,108 @@ +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; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.UUID; +import java.util.function.Consumer; + +import javax.imageio.ImageIO; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.ui.controls.Dialogs; +import ctbrec.ui.controls.FileSelectionBox; +import javafx.scene.Cursor; +import javafx.scene.Node; + +public class SetPortraitAction { + 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) { + this.source = source; + this.model = selectedModel; + this.callback = callback; + } + + public void execute() { + source.setCursor(Cursor.WAIT); + String portraitId = Config.getInstance().getSettings().modelPortraits.getOrDefault(model.getUrl(), UUID.randomUUID().toString()); + + FileSelectionBox portraitSelectionBox = new FileSelectionBox(); + boolean accepted = Dialogs.showCustomInput(source.getScene(), "Select a portrait image", portraitSelectionBox); + if (!accepted) { + return; + } + String selectedFile = portraitSelectionBox.fileProperty().getValue(); + + LOG.debug("User selected {}", selectedFile); + boolean success = processImageFile(portraitId, selectedFile); + if (success) { + Config.getInstance().getSettings().modelPortraits.put(model.getUrl(), portraitId); + try { + Config.getInstance().save(); + runCallback(); + } catch (IOException e) { + Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e); + } + } + source.setCursor(Cursor.DEFAULT); + } + + private void runCallback() { + if (callback != null) { + try { + callback.accept(model); + } catch (Exception e) { + LOG.error("Error while executing callback", e); + } + } + } + + private boolean processImageFile(String portraitId, String selectedFile) { + try { + BufferedImage portrait = convertToScaledJpg(selectedFile); + boolean success = copyToCacheAsJpg(portraitId, portrait); + if (!success) { + LOG.debug("Available formats: {}", Arrays.toString(ImageIO.getWriterFormatNames())); + throw new IOException("No suitable writer found for image format " + FORMAT); + } + return success; + } catch (IOException e) { + LOG.error("Error while changing portrait image", e); + Dialogs.showError("Set Portrait", "Couldn't change portrait image: ", e); + 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 configDir = Config.getInstance().getConfigDir(); + File portraitDir = new File(configDir, "portraits"); + Files.createDirectories(portraitDir.toPath()); + File output = new File(portraitDir, portraitId + '.' + FORMAT); + LOG.debug("Writing scaled portrait to {}", output); + return ImageIO.write(portrait, FORMAT, output); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java index b094a3c4..5c6477cd 100644 --- a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -45,6 +45,7 @@ public abstract class AbstractFileSelectionBox extends HBox { }); var browse = createBrowseButton(); browse.disableProperty().bind(disableProperty()); + browse.prefHeightProperty().bind(fileInput.prefWidthProperty()); fileInput.disableProperty().bind(disableProperty()); fileInput.textProperty().bindBidirectional(fileProperty); getChildren().addAll(fileInput, browse); diff --git a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java index 9920c12a..694e2adf 100644 --- a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java +++ b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java @@ -30,6 +30,7 @@ import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.RemoveTimeLimitAction; import ctbrec.ui.action.ResumeAction; +import ctbrec.ui.action.SetPortraitAction; import ctbrec.ui.action.SetStopDateAction; import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.action.StopRecordingAction; @@ -61,6 +62,7 @@ public class ModelMenuContributor { private Consumer startStopCallback; private TriConsumer followCallback; private Consumer ignoreCallback; + private Consumer portraitCallback; private boolean removeWithIgnore = false; private Runnable callback; @@ -89,6 +91,12 @@ public class ModelMenuContributor { return this; } + public ModelMenuContributor withPortraitCallback(Consumer portraitCallback) { + this.portraitCallback = portraitCallback; + return this; + } + + public ModelMenuContributor removeModelAfterIgnore(boolean yes) { this.removeWithIgnore = yes; return this; @@ -98,6 +106,7 @@ public class ModelMenuContributor { startStopCallback = Optional.ofNullable(startStopCallback).orElse(m -> {}); followCallback = Optional.ofNullable(followCallback).orElse((m, f, s) -> {}); ignoreCallback = Optional.ofNullable(ignoreCallback).orElse(m -> {}); + portraitCallback = Optional.ofNullable(portraitCallback).orElse(m -> {}); callback = Optional.ofNullable(callback).orElse(() -> {}); addOpenInPlayer(menu, selectedModels); addOpenInBrowser(menu, selectedModels); @@ -119,6 +128,7 @@ public class ModelMenuContributor { addIgnore(menu, selectedModels); addOpenRecDir(menu, selectedModels); addNotes(menu, selectedModels); + addPortrait(menu, selectedModels); } public ModelMenuContributor afterwards(Runnable callback) { @@ -133,6 +143,13 @@ public class ModelMenuContributor { menu.getItems().add(notes); } + private void addPortrait(ContextMenu menu, List selectedModels) { + var portrait = new MenuItem("Portrait"); + portrait.setDisable(selectedModels.size() != 1); + portrait.setOnAction(e -> new SetPortraitAction(source, selectedModels.get(0), portraitCallback).execute()); + menu.getItems().add(portrait); + } + private void addOpenRecDir(ContextMenu menu, List selectedModels) { if (selectedModels == null || selectedModels.isEmpty()) { return; diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/ImageTableCell.java b/client/src/main/java/ctbrec/ui/tabs/recorded/ImageTableCell.java new file mode 100644 index 00000000..030d5c0b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/ImageTableCell.java @@ -0,0 +1,31 @@ +package ctbrec.ui.tabs.recorded; + +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +public class ImageTableCell extends ClickableTableCell { + + protected ImageView imageView; + + public ImageTableCell() { + imageView = new ImageView(); + imageView.setSmooth(true); + imageView.setPreserveRatio(true); + imageView.prefHeight(64); + imageView.setFitHeight(64); + setGraphic(imageView); + } + + @Override + public void requestLayout() { + double columnWidth = getTableColumn().getWidth(); + imageView.prefHeight(columnWidth); + imageView.setFitHeight(columnWidth); + super.requestLayout(); + } + + @Override + protected void updateItem(Image image, boolean empty) { + imageView.setImage(empty ? null : image); + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java index 094df8c7..30bf30a8 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -2,6 +2,9 @@ package ctbrec.ui.tabs.recorded; import static ctbrec.Recording.State.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -11,6 +14,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -21,6 +25,10 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + import ctbrec.Config; import ctbrec.Model; import ctbrec.ModelGroup; @@ -35,6 +43,7 @@ import ctbrec.ui.action.CheckModelAccountAction; import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.ResumeAction; +import ctbrec.ui.action.SetPortraitAction; import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.action.ToggleRecordingAction; import ctbrec.ui.controls.CustomMouseBehaviorContextMenu; @@ -78,6 +87,7 @@ import javafx.scene.control.Tooltip; import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.image.Image; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -119,6 +129,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { Button checkModelAccountExistance = new Button("Check URLs"); TextField filter; + LoadingCache portraitCache = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.DAYS) + .maximumSize(1000) + .build(CacheLoader.from(RecordedModelsTab::loadModelPortrait)); + public RecordedModelsTab(String title, Recorder recorder, List sites) { super(title); this.recorder = recorder; @@ -155,6 +170,23 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { if (!Config.getInstance().getSettings().livePreviews) { preview.setVisible(false); } + + TableColumn portrait = new TableColumn<>("Portrait"); + portrait.setPrefWidth(80); + portrait.setCellValueFactory(param -> { + Model mdl = param.getValue().getDelegate(); + Image image = null; + try { + image = portraitCache.get(mdl); + } catch (ExecutionException e) { + LOG.error("Error while loading portrait from cache for {}", mdl, e); + } + return new SimpleObjectProperty(image); + }); + portrait.setCellFactory(param -> new ImageTableCell()); + portrait.setEditable(false); + portrait.setId("portrait"); + TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); name.setCellValueFactory(param -> { @@ -235,7 +267,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { notes.setPrefWidth(400); notes.setEditable(false); notes.setId("notes"); - table.getColumns().addAll(preview, name, url, online, recording, paused, priority, lastSeen, lastRecorded, notes); + table.getColumns().addAll(preview, portrait, name, url, online, recording, paused, priority, lastSeen, lastRecorded, notes); table.setItems(observableModels); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); @@ -632,6 +664,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) // .withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) // .removeModelAfterIgnore(true) // + .withPortraitCallback(m -> { + portraitCache.invalidate(m); + table.refresh(); + }) .afterwards(table::refresh) // .contributeToMenu(selectedModels, menu); @@ -796,4 +832,19 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return s; } } + + private static Image loadModelPortrait(Model model) { + String portraitId = Config.getInstance().getSettings().modelPortraits.get(model.getUrl()); + if (StringUtil.isNotBlank(portraitId)) { + File configDir = Config.getInstance().getConfigDir(); + File portraitDir = new File(configDir, "portraits"); + File portraitFile = new File(portraitDir, portraitId + '.' + SetPortraitAction.FORMAT); + try { + return new Image(new FileInputStream(portraitFile)); + } catch (FileNotFoundException e) { + LOG.error("Couldn't load portrait file {}", portraitFile, e); + } + } + return new Image(RecordedModelsTab.class.getResourceAsStream("/silhouette_256.png")); + } } diff --git a/client/src/main/resources/silhouette_256.png b/client/src/main/resources/silhouette_256.png new file mode 100644 index 0000000000000000000000000000000000000000..5a05ba3f993b071a98c4b12f053dcdbe7b3d5692 GIT binary patch literal 9373 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 literal 0 HcmV?d00001 diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index 216f4c72..700a529b 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -84,7 +84,7 @@ public abstract class AbstractModel implements Model { @Override public String getSanitizedNamed() { String sanitizedName = Optional.ofNullable(getName()).orElse(""); - return sanitizedName.replace(' ', '_').replace('\\', '_').replace('/', '_'); + return sanitizedName.replaceAll("[^a-zA-Z0-9.-]", "_"); } @Override diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index aa05d35c..f2af3d49 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -113,9 +113,10 @@ public class Settings { @Deprecated public int minimumLengthInSeconds = 0; public long minimumSpaceLeftInBytes = 0; - public Map modelNotes = new HashMap<>(); public List models = new ArrayList<>(); public Set modelGroups = new HashSet<>(); + public Map modelNotes = new HashMap<>(); + public Map modelPortraits = new HashMap<>(); @Deprecated public List modelsIgnored = new ArrayList<>(); public boolean monitorClipboard = false;