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();
|
var browse = createBrowseButton();
|
||||||
browse.disableProperty().bind(disableProperty());
|
browse.disableProperty().bind(disableProperty());
|
||||||
|
browse.prefHeightProperty().bind(fileInput.prefWidthProperty());
|
||||||
fileInput.disableProperty().bind(disableProperty());
|
fileInput.disableProperty().bind(disableProperty());
|
||||||
fileInput.textProperty().bindBidirectional(fileProperty);
|
fileInput.textProperty().bindBidirectional(fileProperty);
|
||||||
getChildren().addAll(fileInput, browse);
|
getChildren().addAll(fileInput, browse);
|
||||||
|
|
|
@ -30,6 +30,7 @@ import ctbrec.ui.action.PauseAction;
|
||||||
import ctbrec.ui.action.PlayAction;
|
import ctbrec.ui.action.PlayAction;
|
||||||
import ctbrec.ui.action.RemoveTimeLimitAction;
|
import ctbrec.ui.action.RemoveTimeLimitAction;
|
||||||
import ctbrec.ui.action.ResumeAction;
|
import ctbrec.ui.action.ResumeAction;
|
||||||
|
import ctbrec.ui.action.SetPortraitAction;
|
||||||
import ctbrec.ui.action.SetStopDateAction;
|
import ctbrec.ui.action.SetStopDateAction;
|
||||||
import ctbrec.ui.action.StartRecordingAction;
|
import ctbrec.ui.action.StartRecordingAction;
|
||||||
import ctbrec.ui.action.StopRecordingAction;
|
import ctbrec.ui.action.StopRecordingAction;
|
||||||
|
@ -61,6 +62,7 @@ public class ModelMenuContributor {
|
||||||
private Consumer<Model> startStopCallback;
|
private Consumer<Model> startStopCallback;
|
||||||
private TriConsumer<Model, Boolean, Boolean> followCallback;
|
private TriConsumer<Model, Boolean, Boolean> followCallback;
|
||||||
private Consumer<Model> ignoreCallback;
|
private Consumer<Model> ignoreCallback;
|
||||||
|
private Consumer<Model> portraitCallback;
|
||||||
private boolean removeWithIgnore = false;
|
private boolean removeWithIgnore = false;
|
||||||
private Runnable callback;
|
private Runnable callback;
|
||||||
|
|
||||||
|
@ -89,6 +91,12 @@ public class ModelMenuContributor {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ModelMenuContributor withPortraitCallback(Consumer<Model> portraitCallback) {
|
||||||
|
this.portraitCallback = portraitCallback;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public ModelMenuContributor removeModelAfterIgnore(boolean yes) {
|
public ModelMenuContributor removeModelAfterIgnore(boolean yes) {
|
||||||
this.removeWithIgnore = yes;
|
this.removeWithIgnore = yes;
|
||||||
return this;
|
return this;
|
||||||
|
@ -98,6 +106,7 @@ public class ModelMenuContributor {
|
||||||
startStopCallback = Optional.ofNullable(startStopCallback).orElse(m -> {});
|
startStopCallback = Optional.ofNullable(startStopCallback).orElse(m -> {});
|
||||||
followCallback = Optional.ofNullable(followCallback).orElse((m, f, s) -> {});
|
followCallback = Optional.ofNullable(followCallback).orElse((m, f, s) -> {});
|
||||||
ignoreCallback = Optional.ofNullable(ignoreCallback).orElse(m -> {});
|
ignoreCallback = Optional.ofNullable(ignoreCallback).orElse(m -> {});
|
||||||
|
portraitCallback = Optional.ofNullable(portraitCallback).orElse(m -> {});
|
||||||
callback = Optional.ofNullable(callback).orElse(() -> {});
|
callback = Optional.ofNullable(callback).orElse(() -> {});
|
||||||
addOpenInPlayer(menu, selectedModels);
|
addOpenInPlayer(menu, selectedModels);
|
||||||
addOpenInBrowser(menu, selectedModels);
|
addOpenInBrowser(menu, selectedModels);
|
||||||
|
@ -119,6 +128,7 @@ public class ModelMenuContributor {
|
||||||
addIgnore(menu, selectedModels);
|
addIgnore(menu, selectedModels);
|
||||||
addOpenRecDir(menu, selectedModels);
|
addOpenRecDir(menu, selectedModels);
|
||||||
addNotes(menu, selectedModels);
|
addNotes(menu, selectedModels);
|
||||||
|
addPortrait(menu, selectedModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModelMenuContributor afterwards(Runnable callback) {
|
public ModelMenuContributor afterwards(Runnable callback) {
|
||||||
|
@ -133,6 +143,13 @@ public class ModelMenuContributor {
|
||||||
menu.getItems().add(notes);
|
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) {
|
private void addOpenRecDir(ContextMenu menu, List<Model> selectedModels) {
|
||||||
if (selectedModels == null || selectedModels.isEmpty()) {
|
if (selectedModels == null || selectedModels.isEmpty()) {
|
||||||
return;
|
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 static ctbrec.Recording.State.*;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
@ -11,6 +14,7 @@ import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -21,6 +25,10 @@ import java.util.stream.Collectors;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.ModelGroup;
|
import ctbrec.ModelGroup;
|
||||||
|
@ -35,6 +43,7 @@ import ctbrec.ui.action.CheckModelAccountAction;
|
||||||
import ctbrec.ui.action.PauseAction;
|
import ctbrec.ui.action.PauseAction;
|
||||||
import ctbrec.ui.action.PlayAction;
|
import ctbrec.ui.action.PlayAction;
|
||||||
import ctbrec.ui.action.ResumeAction;
|
import ctbrec.ui.action.ResumeAction;
|
||||||
|
import ctbrec.ui.action.SetPortraitAction;
|
||||||
import ctbrec.ui.action.StopRecordingAction;
|
import ctbrec.ui.action.StopRecordingAction;
|
||||||
import ctbrec.ui.action.ToggleRecordingAction;
|
import ctbrec.ui.action.ToggleRecordingAction;
|
||||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
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.CheckBoxTableCell;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
import javafx.scene.control.cell.TextFieldTableCell;
|
import javafx.scene.control.cell.TextFieldTableCell;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.input.ContextMenuEvent;
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
|
@ -119,6 +129,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
Button checkModelAccountExistance = new Button("Check URLs");
|
Button checkModelAccountExistance = new Button("Check URLs");
|
||||||
TextField filter;
|
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) {
|
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
|
||||||
super(title);
|
super(title);
|
||||||
this.recorder = recorder;
|
this.recorder = recorder;
|
||||||
|
@ -155,6 +170,23 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
if (!Config.getInstance().getSettings().livePreviews) {
|
if (!Config.getInstance().getSettings().livePreviews) {
|
||||||
preview.setVisible(false);
|
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");
|
TableColumn<JavaFxModel, ModelName> name = new TableColumn<>("Model");
|
||||||
name.setPrefWidth(200);
|
name.setPrefWidth(200);
|
||||||
name.setCellValueFactory(param -> {
|
name.setCellValueFactory(param -> {
|
||||||
|
@ -235,7 +267,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
notes.setPrefWidth(400);
|
notes.setPrefWidth(400);
|
||||||
notes.setEditable(false);
|
notes.setEditable(false);
|
||||||
notes.setId("notes");
|
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.setItems(observableModels);
|
||||||
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||||
popup = createContextMenu();
|
popup = createContextMenu();
|
||||||
|
@ -632,6 +664,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
|
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
|
||||||
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
|
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
|
||||||
.removeModelAfterIgnore(true) //
|
.removeModelAfterIgnore(true) //
|
||||||
|
.withPortraitCallback(m -> {
|
||||||
|
portraitCache.invalidate(m);
|
||||||
|
table.refresh();
|
||||||
|
})
|
||||||
.afterwards(table::refresh) //
|
.afterwards(table::refresh) //
|
||||||
.contributeToMenu(selectedModels, menu);
|
.contributeToMenu(selectedModels, menu);
|
||||||
|
|
||||||
|
@ -796,4 +832,19 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
return s;
|
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
|
@Override
|
||||||
public String getSanitizedNamed() {
|
public String getSanitizedNamed() {
|
||||||
String sanitizedName = Optional.ofNullable(getName()).orElse("");
|
String sanitizedName = Optional.ofNullable(getName()).orElse("");
|
||||||
return sanitizedName.replace(' ', '_').replace('\\', '_').replace('/', '_');
|
return sanitizedName.replaceAll("[^a-zA-Z0-9.-]", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -113,9 +113,10 @@ public class Settings {
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public int minimumLengthInSeconds = 0;
|
public int minimumLengthInSeconds = 0;
|
||||||
public long minimumSpaceLeftInBytes = 0;
|
public long minimumSpaceLeftInBytes = 0;
|
||||||
public Map<String, String> modelNotes = new HashMap<>();
|
|
||||||
public List<Model> models = new ArrayList<>();
|
public List<Model> models = new ArrayList<>();
|
||||||
public Set<ModelGroup> modelGroups = new HashSet<>();
|
public Set<ModelGroup> modelGroups = new HashSet<>();
|
||||||
|
public Map<String, String> modelNotes = new HashMap<>();
|
||||||
|
public Map<String, String> modelPortraits = new HashMap<>();
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public List<Model> modelsIgnored = new ArrayList<>();
|
public List<Model> modelsIgnored = new ArrayList<>();
|
||||||
public boolean monitorClipboard = false;
|
public boolean monitorClipboard = false;
|
||||||
|
|
Loading…
Reference in New Issue