forked from j62/ctbrec
1
0
Fork 0

Add first working client side version for model portraits

This commit is contained in:
0xb00bface 2021-08-15 14:12:47 +02:00
parent d9742f5962
commit 28ca1932e9
8 changed files with 212 additions and 3 deletions

View File

@ -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<Model> callback;
public SetPortraitAction(Node source, Model selectedModel, Consumer<Model> 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);
}
}

View File

@ -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);

View File

@ -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<Model> startStopCallback;
private TriConsumer<Model, Boolean, Boolean> followCallback;
private Consumer<Model> ignoreCallback;
private Consumer<Model> portraitCallback;
private boolean removeWithIgnore = false;
private Runnable callback;
@ -89,6 +91,12 @@ public class ModelMenuContributor {
return this;
}
public ModelMenuContributor withPortraitCallback(Consumer<Model> 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<Model> 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<Model> selectedModels) {
if (selectedModels == null || selectedModels.isEmpty()) {
return;

View File

@ -0,0 +1,31 @@
package ctbrec.ui.tabs.recorded;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
public class ImageTableCell extends ClickableTableCell<Image> {
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);
}
}

View File

@ -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<Model, Image> portraitCache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.DAYS)
.maximumSize(1000)
.build(CacheLoader.from(RecordedModelsTab::loadModelPortrait));
public RecordedModelsTab(String title, Recorder recorder, List<Site> 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<JavaFxModel, Image> 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>(image);
});
portrait.setCellFactory(param -> new ImageTableCell());
portrait.setEditable(false);
portrait.setId("portrait");
TableColumn<JavaFxModel, ModelName> 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"));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -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

View File

@ -113,9 +113,10 @@ public class Settings {
@Deprecated
public int minimumLengthInSeconds = 0;
public long minimumSpaceLeftInBytes = 0;
public Map<String, String> modelNotes = new HashMap<>();
public List<Model> models = new ArrayList<>();
public Set<ModelGroup> modelGroups = new HashSet<>();
public Map<String, String> modelNotes = new HashMap<>();
public Map<String, String> modelPortraits = new HashMap<>();
@Deprecated
public List<Model> modelsIgnored = new ArrayList<>();
public boolean monitorClipboard = false;