forked from j62/ctbrec
Add first working client side version for model portraits
This commit is contained in:
parent
d9742f5962
commit
28ca1932e9
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue