Add export and import function for the model lists (recording and later)
This commit is contained in:
parent
0d512134ed
commit
f04eb5310e
|
@ -2,6 +2,7 @@ package ctbrec.ui.controls;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -29,17 +30,19 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
|||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractFileSelectionBox.class);
|
||||
|
||||
private StringProperty fileProperty = new SimpleStringProperty();
|
||||
private final StringProperty fileProperty = new SimpleStringProperty();
|
||||
private final Tooltip validationError = new Tooltip();
|
||||
protected TextField fileInput;
|
||||
protected boolean allowEmptyValue = false;
|
||||
private Tooltip validationError = new Tooltip();
|
||||
protected boolean saveDialog = false;
|
||||
protected boolean validationDisabled = false;
|
||||
|
||||
protected AbstractFileSelectionBox() {
|
||||
super(5);
|
||||
fileInput = new TextField();
|
||||
fileInput.textProperty().addListener(textListener());
|
||||
fileInput.focusedProperty().addListener((obs, o, n) -> {
|
||||
if (!n.booleanValue()) {
|
||||
if (Objects.equals(Boolean.FALSE, n)) {
|
||||
validationError.hide();
|
||||
}
|
||||
});
|
||||
|
@ -52,7 +55,7 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
|||
HBox.setHgrow(fileInput, Priority.ALWAYS);
|
||||
|
||||
disabledProperty().addListener((obs, oldV, newV) -> {
|
||||
if (newV.booleanValue()) {
|
||||
if (Objects.equals(Boolean.TRUE, newV)) {
|
||||
hideValidationHints();
|
||||
} else {
|
||||
if (StringUtil.isNotBlank(fileInput.getText())) {
|
||||
|
@ -74,7 +77,6 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
|||
if (allowEmptyValue) {
|
||||
fileProperty.set("");
|
||||
hideValidationHints();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
var program = new File(input);
|
||||
|
@ -106,7 +108,7 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
|||
}
|
||||
|
||||
protected String validate(File file) {
|
||||
if (isDisabled()) {
|
||||
if (isDisabled() || validationDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -121,6 +123,14 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
|||
this.allowEmptyValue = true;
|
||||
}
|
||||
|
||||
public void useSaveDialog() {
|
||||
this.saveDialog = true;
|
||||
}
|
||||
|
||||
public void disableValidation() {
|
||||
validationDisabled = true;
|
||||
}
|
||||
|
||||
private Button createBrowseButton() {
|
||||
var button = new Button("Select");
|
||||
button.setOnAction(e -> choose());
|
||||
|
@ -131,7 +141,12 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
|||
|
||||
protected void choose() {
|
||||
var chooser = new FileChooser();
|
||||
var program = chooser.showOpenDialog(null);
|
||||
File program;
|
||||
if (saveDialog) {
|
||||
program = chooser.showSaveDialog(null);
|
||||
} else {
|
||||
program = chooser.showOpenDialog(null);
|
||||
}
|
||||
if (program != null) {
|
||||
try {
|
||||
fileInput.setText(program.getCanonicalPath());
|
||||
|
|
|
@ -1,37 +1,23 @@
|
|||
package ctbrec.ui.controls;
|
||||
|
||||
import static javafx.scene.control.ButtonType.*;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
|
||||
import static javafx.scene.control.ButtonType.*;
|
||||
|
||||
public class Dialogs {
|
||||
|
||||
private Dialogs() {}
|
||||
|
@ -149,36 +135,4 @@ public class Dialogs {
|
|||
confirm.showAndWait();
|
||||
return confirm.getResult();
|
||||
}
|
||||
|
||||
public static Optional<ModelGroup> showModelGroupSelectionDialog(Scene parent, Model model) {
|
||||
var dialogPane = new GridPane();
|
||||
Set<ModelGroup> modelGroups = Config.getInstance().getSettings().modelGroups;
|
||||
ObservableList<ModelGroup> comboBoxModel = FXCollections.observableArrayList(modelGroups);
|
||||
ComboBox<ModelGroup> comboBox = new ComboBox<>(comboBoxModel);
|
||||
comboBox.setEditable(true);
|
||||
comboBox.setPlaceholder(new Label(" type in a name to a add a new group "));
|
||||
dialogPane.add(new Label("Model group"), 0, 0);
|
||||
dialogPane.add(comboBox, 1, 0);
|
||||
boolean ok = showCustomInput(parent, "Add model to group", dialogPane);
|
||||
if (ok) {
|
||||
String text = comboBox.getEditor().getText();
|
||||
if (StringUtil.isBlank(text)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<ModelGroup> existingGroup = modelGroups.stream().filter(mg -> mg.getName().equalsIgnoreCase(text)).findFirst();
|
||||
if (existingGroup.isPresent()) {
|
||||
existingGroup.get().add(model);
|
||||
return existingGroup;
|
||||
} else {
|
||||
var group = new ModelGroup();
|
||||
group.setId(UUID.randomUUID());
|
||||
group.setName(text);
|
||||
group.add(model);
|
||||
modelGroups.add(group);
|
||||
return Optional.of(group);
|
||||
}
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ public class FileSelectionBox extends AbstractFileSelectionBox {
|
|||
|
||||
@Override
|
||||
protected String validate(File file) {
|
||||
if (isDisabled()) {
|
||||
if (isDisabled() || validationDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,39 +28,43 @@ import ctbrec.ui.controls.table.SettingTableViewStateStore;
|
|||
import ctbrec.ui.controls.table.StatePersistingTableView;
|
||||
import ctbrec.ui.menu.ModelMenuContributor;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import ctbrec.ui.tabs.recorded.ModelImportExport.ExportOptions;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ctbrec.ui.action.AbstractPortraitAction.FORMAT;
|
||||
import static ctbrec.ui.tabs.recorded.ModelImportExport.ExportIncludes.*;
|
||||
|
||||
public abstract class AbstractRecordedModelsTab extends Tab implements TabSelectionListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractRecordedModelsTab.class);
|
||||
|
@ -77,7 +81,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
|||
protected LoadingCache<Model, Image> portraitCache = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.DAYS)
|
||||
.maximumSize(1000)
|
||||
.build(CacheLoader.from(AbstractRecordedModelsTab::loadModelPortrait));
|
||||
.build(CacheLoader.from(this::loadModelPortrait));
|
||||
|
||||
protected AutoFillTextField modelInputField;
|
||||
protected List<Site> sites;
|
||||
|
@ -88,6 +92,8 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
|||
protected Label modelLabel = new Label("Model");
|
||||
protected Button addModelButton = new Button("Record");
|
||||
protected Button checkModelAccountExistance = new Button("Check URLs");
|
||||
protected Button exportModelsButton = new Button();
|
||||
protected Button importModelsButton = new Button();
|
||||
protected TextField filter;
|
||||
|
||||
protected FlowPane grid = new FlowPane();
|
||||
|
@ -95,10 +101,12 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
|||
protected ContextMenu popup;
|
||||
|
||||
protected Config config;
|
||||
protected PortraitStore portraitStore;
|
||||
|
||||
AbstractRecordedModelsTab(String text, String stateStorePrefix) {
|
||||
super(text);
|
||||
config = Config.getInstance();
|
||||
portraitStore = new PortraitStore(config);
|
||||
tableStateStore = new SettingTableViewStateStore(config, stateStorePrefix);
|
||||
table = new StatePersistingTableView<>(tableStateStore);
|
||||
registerPortraitListener();
|
||||
|
@ -178,7 +186,20 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
|||
BorderPane.setMargin(addModelBox, new Insets(5));
|
||||
addModelButton.setOnAction(this::addModel);
|
||||
addModelButton.setPadding(new Insets(5));
|
||||
addModelBox.getChildren().addAll(modelLabel, modelInputField, addModelButton, checkModelAccountExistance);
|
||||
ImageView exportIcon = new ImageView(Objects.requireNonNull(getClass().getResource("/16/download.png"), "/16/download.png not found").toString());
|
||||
exportModelsButton.setGraphic(exportIcon);
|
||||
exportModelsButton.setTooltip(new Tooltip("Export models to file"));
|
||||
exportModelsButton.setMinWidth(34);
|
||||
exportModelsButton.setMinHeight(26);
|
||||
exportModelsButton.setOnAction(this::exportModels);
|
||||
ImageView importIcon = new ImageView(Objects.requireNonNull(getClass().getResource("/16/upload.png"), "/16/upload.png not found").toString());
|
||||
importModelsButton.setGraphic(importIcon);
|
||||
importModelsButton.setTooltip(new Tooltip("Import models from file"));
|
||||
importModelsButton.setMinWidth(34);
|
||||
importModelsButton.setMinHeight(26);
|
||||
importModelsButton.setOnAction(this::importModels);
|
||||
HBox.setMargin(exportModelsButton, new Insets(0, 0, 0, 20));
|
||||
addModelBox.getChildren().addAll(modelLabel, modelInputField, addModelButton, checkModelAccountExistance, exportModelsButton, importModelsButton);
|
||||
|
||||
filterContainer.setPadding(new Insets(0));
|
||||
filterContainer.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
@ -204,6 +225,59 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
|||
addModelBox.getChildren().add(filterContainer);
|
||||
}
|
||||
|
||||
protected abstract List<Model> getExportList();
|
||||
|
||||
private void exportModels(ActionEvent actionEvent) {
|
||||
ExportOptions exportOptions = new ModelExportDialog(getTabPane()).showAndWait();
|
||||
if (exportOptions != null) {
|
||||
try {
|
||||
ModelImportExport.exportTo(getExportList(), config, exportOptions);
|
||||
} catch (IOException e) {
|
||||
String msg = "An error occurred while exporting the model list";
|
||||
Dialogs.showError(getTabPane().getScene(), "Export models", msg, e);
|
||||
LOG.error(msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void importModelList(List<Model> models) {
|
||||
getContent().setCursor(Cursor.WAIT);
|
||||
Task<Void> task = new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
for (Model model : models) {
|
||||
try {
|
||||
recorder.addModel(model);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Couldn't add model to recording list", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
getContent().setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
};
|
||||
GlobalThreadPool.submit(task);
|
||||
}
|
||||
|
||||
private void importModels(ActionEvent actionEvent) {
|
||||
var chooser = new FileChooser();
|
||||
File target = chooser.showOpenDialog(getTabPane().getScene().getWindow());
|
||||
if (target != null) {
|
||||
try {
|
||||
List<Model> models = ModelImportExport.importFrom(target, sites, config);
|
||||
importModelList(models);
|
||||
} catch (IOException e) {
|
||||
String msg = "An error occurred while importing the model list";
|
||||
Dialogs.showError(getTabPane().getScene(), "Import models", msg, e);
|
||||
LOG.error(msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void addPreviewColumn(int columnIdx) {
|
||||
TableColumn<JavaFxModel, String> preview = addTableColumn("preview", "🎥", columnIdx, 35);
|
||||
preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ "));
|
||||
|
@ -292,7 +366,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
|||
}
|
||||
|
||||
protected ContextMenu createContextMenu() {
|
||||
List<Model> selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
|
||||
List<Model> selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).toList();
|
||||
if (selectedModels.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
@ -487,18 +561,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
|
|||
}
|
||||
}
|
||||
|
||||
protected 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 + '.' + FORMAT);
|
||||
try {
|
||||
return new Image(new FileInputStream(portraitFile));
|
||||
} catch (FileNotFoundException e) {
|
||||
LOG.error("Couldn't load portrait file {}", portraitFile, e);
|
||||
}
|
||||
}
|
||||
return SILHOUETTE;
|
||||
protected Image loadModelPortrait(Model model) {
|
||||
return portraitStore.loadModelPortrait(model.getUrl()).orElse(SILHOUETTE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.FileSelectionBox;
|
||||
import ctbrec.ui.tabs.recorded.ModelImportExport.ExportIncludes;
|
||||
import ctbrec.ui.tabs.recorded.ModelImportExport.ExportOptions;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static ctbrec.ui.tabs.recorded.ModelImportExport.ExportIncludes.*;
|
||||
|
||||
public class ModelExportDialog {
|
||||
|
||||
private final Node source;
|
||||
private final GridPane gridPane = new GridPane();
|
||||
|
||||
private CheckBox notesButton;
|
||||
private CheckBox groupsButton;
|
||||
private CheckBox portraisButton;
|
||||
private FileSelectionBox fileSelectionBox;
|
||||
|
||||
public ModelExportDialog(Node source) {
|
||||
this.source = source;
|
||||
createGui();
|
||||
}
|
||||
|
||||
private void createGui() {
|
||||
source.setCursor(Cursor.WAIT);
|
||||
gridPane.setHgap(10);
|
||||
gridPane.setVgap(10);
|
||||
gridPane.setPadding(new Insets(20, 150, 10, 10));
|
||||
Label l = new Label("Export to file");
|
||||
gridPane.add(l, 0, 0);
|
||||
fileSelectionBox = new FileSelectionBox();
|
||||
fileSelectionBox.useSaveDialog();
|
||||
fileSelectionBox.disableValidation();
|
||||
gridPane.add(fileSelectionBox, 1, 0);
|
||||
GridPane.setValignment(l, VPos.TOP);
|
||||
notesButton = new CheckBox("notes");
|
||||
notesButton.setSelected(true);
|
||||
groupsButton = new CheckBox("groups");
|
||||
groupsButton.setSelected(true);
|
||||
portraisButton = new CheckBox("portraits");
|
||||
portraisButton.setSelected(true);
|
||||
var row = new VBox();
|
||||
row.getChildren().addAll(notesButton, groupsButton, portraisButton);
|
||||
VBox.setMargin(notesButton, new Insets(5));
|
||||
VBox.setMargin(groupsButton, new Insets(5));
|
||||
VBox.setMargin(portraisButton, new Insets(5));
|
||||
gridPane.add(row, 1, 1);
|
||||
}
|
||||
|
||||
public ExportOptions showAndWait() {
|
||||
try {
|
||||
boolean confirmed = Dialogs.showCustomInput(source.getScene(), "Export model list", gridPane);
|
||||
if (confirmed) {
|
||||
Set<ExportIncludes> exportIncludes = new HashSet<>();
|
||||
if (notesButton.isSelected()) {
|
||||
exportIncludes.add(NOTES);
|
||||
}
|
||||
if (groupsButton.isSelected()) {
|
||||
exportIncludes.add(GROUPS);
|
||||
}
|
||||
if (portraisButton.isSelected()) {
|
||||
exportIncludes.add(PORTRAITS);
|
||||
}
|
||||
return new ExportOptions(exportIncludes, new File(fileSelectionBox.fileProperty().getValue()));
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
source.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import com.squareup.moshi.*;
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelGroup;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.LocalTimeJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.io.UuidJSonAdapter;
|
||||
import ctbrec.sites.Site;
|
||||
import okio.Buffer;
|
||||
import okio.Okio;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
|
||||
public class ModelImportExport {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ModelImportExport.class);
|
||||
|
||||
enum ExportIncludes {
|
||||
NOTES,
|
||||
GROUPS,
|
||||
PORTRAITS
|
||||
}
|
||||
|
||||
record ExportOptions(Set<ExportIncludes> includes, File targetFile) {
|
||||
}
|
||||
|
||||
private ModelImportExport() {
|
||||
}
|
||||
|
||||
public static void exportTo(List<Model> models, Config config, ExportOptions exportOptions) throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter())
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.add(UUID.class, new UuidJSonAdapter())
|
||||
.add(LocalTime.class, new LocalTimeJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Map<String, String>> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class));
|
||||
JsonAdapter<List<Model>> modelListAdapter = moshi.adapter(Types.newParameterizedType(List.class, Model.class));
|
||||
JsonAdapter<Set<ModelGroup>> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class));
|
||||
|
||||
try (JsonWriter writer = JsonWriter.of(Okio.buffer(Okio.sink(exportOptions.targetFile())))) {
|
||||
writer.setIndent(" ");
|
||||
writer.beginObject();
|
||||
writer.name("models");
|
||||
modelListAdapter.toJson(writer, models);
|
||||
if (exportOptions.includes().contains(ExportIncludes.NOTES)) {
|
||||
writer.name("notes");
|
||||
notesAdapter.toJson(writer, config.getSettings().modelNotes);
|
||||
}
|
||||
if (exportOptions.includes().contains(ExportIncludes.GROUPS)) {
|
||||
writer.name("groups");
|
||||
modelGroupAdapter.toJson(writer, config.getSettings().modelGroups);
|
||||
}
|
||||
if (exportOptions.includes().contains(ExportIncludes.PORTRAITS)) {
|
||||
var portraits = config.getSettings().modelPortraits;
|
||||
var portraitLoader = new PortraitStore(config);
|
||||
if (portraits != null && !portraits.isEmpty()) {
|
||||
writer.name("portraits");
|
||||
writer.beginArray();
|
||||
for (Map.Entry<String, String> entry : config.getSettings().modelPortraits.entrySet()) {
|
||||
String modelUrl = entry.getKey();
|
||||
String portraitId = entry.getValue();
|
||||
Optional<byte[]> portrait = portraitLoader.loadModelPortraitFile(modelUrl);
|
||||
if (portrait.isPresent()) {
|
||||
writer.beginObject();
|
||||
writer.name("url").value(modelUrl);
|
||||
writer.name("id").value(portraitId);
|
||||
writer.name("data").value(Base64.getEncoder().encodeToString(portrait.get()));
|
||||
writer.endObject();
|
||||
}
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
}
|
||||
writer.endObject();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Model> importFrom(File target, List<Site> sites, Config config) throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelJsonAdapter(sites))
|
||||
.add(File.class, new FileJsonAdapter())
|
||||
.add(UUID.class, new UuidJSonAdapter())
|
||||
.add(LocalTime.class, new LocalTimeJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Model> modelAdapter = moshi.adapter(Model.class);
|
||||
JsonAdapter<Map<String, String>> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class));
|
||||
JsonAdapter<Set<ModelGroup>> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class));
|
||||
|
||||
List<Model> models = null;
|
||||
String json = Files.readString(target.toPath(), StandardCharsets.UTF_8);
|
||||
try (Buffer buffer = new Buffer()) {
|
||||
JsonReader reader = JsonReader.of(buffer.writeUtf8(json));
|
||||
reader.setLenient(true);
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
var next = reader.nextName();
|
||||
switch (next) {
|
||||
case "models" -> models = readModels(modelAdapter, reader);
|
||||
case "notes" -> importNotes(reader, notesAdapter, config);
|
||||
case "groups" -> importGroups(reader, modelGroupAdapter, config);
|
||||
case "portraits" -> importPortraits(reader, config);
|
||||
default -> LOG.warn("Element {} unknown", next);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
private static void importPortraits(JsonReader reader, Config config) throws IOException {
|
||||
PortraitStore portraitStore = new PortraitStore(config);
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
reader.beginObject();
|
||||
String url = null;
|
||||
String id = null;
|
||||
String dataBase64 = null;
|
||||
while (reader.hasNext()) {
|
||||
var name = reader.nextName();
|
||||
switch (name) {
|
||||
case "url" -> url = reader.nextString();
|
||||
case "id" -> id = reader.nextString();
|
||||
case "data" -> dataBase64 = reader.nextString();
|
||||
default -> {
|
||||
LOG.warn("Portrait element {} unknown", name);
|
||||
reader.skipValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
portraitStore.writePortrait(id, Base64.getDecoder().decode(dataBase64));
|
||||
config.getSettings().modelPortraits.put(url, id);
|
||||
reader.endObject();
|
||||
}
|
||||
reader.endArray();
|
||||
}
|
||||
|
||||
|
||||
private static void importGroups(JsonReader reader, JsonAdapter<Set<ModelGroup>> modelGroupAdapter, Config config) throws IOException {
|
||||
var groups = modelGroupAdapter.fromJson(reader);
|
||||
if (groups != null) {
|
||||
config.getSettings().modelGroups.addAll(groups);
|
||||
}
|
||||
}
|
||||
|
||||
private static void importNotes(JsonReader reader, JsonAdapter<Map<String, String>> notesAdapter, Config config) throws IOException {
|
||||
var notes = notesAdapter.fromJson(reader);
|
||||
if (notes != null) {
|
||||
config.getSettings().modelNotes.putAll(notes);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Model> readModels(JsonAdapter<Model> modelAdapter, JsonReader reader) throws IOException {
|
||||
List<Model> result = new LinkedList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
try {
|
||||
JsonReader.Token token = reader.peek();
|
||||
if (token == JsonReader.Token.BEGIN_OBJECT) {
|
||||
Model model = modelAdapter.fromJson(reader);
|
||||
result.add(model);
|
||||
} else if (token == JsonReader.Token.NAME) {
|
||||
reader.skipName();
|
||||
} else {
|
||||
reader.skipValue();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Couldn't parse model json", e);
|
||||
}
|
||||
}
|
||||
reader.endArray();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.StringUtil;
|
||||
import javafx.scene.image.Image;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Optional;
|
||||
|
||||
import static ctbrec.ui.action.AbstractPortraitAction.FORMAT;
|
||||
|
||||
public record PortraitStore(Config config) {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PortraitStore.class);
|
||||
|
||||
public Optional<Image> loadModelPortrait(String modelUrl) {
|
||||
return loadModelPortraitFile(modelUrl).map(bytes -> new Image(new ByteArrayInputStream(bytes)));
|
||||
}
|
||||
|
||||
public Optional<byte[]> loadModelPortraitFile(String modelUrl) {
|
||||
String portraitId = config.getSettings().modelPortraits.get(modelUrl);
|
||||
if (StringUtil.isNotBlank(portraitId)) {
|
||||
File configDir = config.getConfigDir();
|
||||
File portraitDir = new File(configDir, "portraits");
|
||||
File portraitFile = new File(portraitDir, portraitId + '.' + FORMAT);
|
||||
try {
|
||||
return Optional.of(Files.readAllBytes(portraitFile.toPath()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't load portrait file {}", portraitFile, e);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void writePortrait(String id, byte[] data) throws IOException {
|
||||
File configDir = config.getConfigDir();
|
||||
File portraitDir = new File(configDir, "portraits");
|
||||
File portraitFile = new File(portraitDir, id + '.' + FORMAT);
|
||||
Files.createDirectories(portraitDir.toPath());
|
||||
Files.write(portraitFile.toPath(), data);
|
||||
}
|
||||
}
|
|
@ -1,18 +1,5 @@
|
|||
package ctbrec.ui.tabs.recorded;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
@ -30,6 +17,18 @@ import javafx.concurrent.WorkerStateEvent;
|
|||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class RecordLaterTab extends AbstractRecordedModelsTab implements TabSelectionListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RecordLaterTab.class);
|
||||
|
@ -69,6 +68,11 @@ public class RecordLaterTab extends AbstractRecordedModelsTab implements TabSele
|
|||
restoreState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Model> getExportList() {
|
||||
return recorder.getModels().stream().filter(Model::isMarkedForLaterRecording).toList();
|
||||
}
|
||||
|
||||
void initializeUpdateService() {
|
||||
updateService = createUpdateService();
|
||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||
|
|
|
@ -38,11 +38,7 @@ public class RecordedModelsPerSiteTab extends RecordedModelsTab implements TabSe
|
|||
protected void pauseAll(ActionEvent evt) {
|
||||
boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models in this table?", getTabPane().getScene());
|
||||
if (yes) {
|
||||
List<Model> models = recorder.getModels().stream()
|
||||
.filter(Predicate.not(Model::isMarkedForLaterRecording))
|
||||
.filter(m -> Objects.equals(m.getSite(), sites.get(0)))
|
||||
.collect(Collectors.toList());
|
||||
new PauseAction(getTabPane(), models, recorder).execute();
|
||||
new PauseAction(getTabPane(), getFilteredModelsForTab(), recorder).execute();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,11 +46,19 @@ public class RecordedModelsPerSiteTab extends RecordedModelsTab implements TabSe
|
|||
protected void resumeAll(ActionEvent evt) {
|
||||
boolean yes = Dialogs.showConfirmDialog("Resume all models", "", "Pause the recording of all models in this table?", getTabPane().getScene());
|
||||
if (yes) {
|
||||
List<Model> models = recorder.getModels().stream()
|
||||
.filter(Predicate.not(Model::isMarkedForLaterRecording))
|
||||
.filter(m -> Objects.equals(m.getSite(), sites.get(0)))
|
||||
.collect(Collectors.toList());
|
||||
new ResumeAction(getTabPane(), models, recorder).execute();
|
||||
new ResumeAction(getTabPane(), getFilteredModelsForTab(), recorder).execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Model> getExportList() {
|
||||
return getFilteredModelsForTab();
|
||||
}
|
||||
|
||||
private List<Model> getFilteredModelsForTab() {
|
||||
return recorder.getModels().stream()
|
||||
.filter(Predicate.not(Model::isMarkedForLaterRecording))
|
||||
.filter(m -> Objects.equals(m.getSite(), sites.get(0)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,6 +169,11 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS
|
|||
restoreState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Model> getExportList() {
|
||||
return recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).toList();
|
||||
}
|
||||
|
||||
private void onUpdatePriority(CellEditEvent<JavaFxModel, Number> evt) {
|
||||
try {
|
||||
int prio = Optional.ofNullable(evt.getNewValue()).map(Number::intValue).orElse(-1);
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 354 B |
Binary file not shown.
After Width: | Height: | Size: 349 B |
|
@ -265,12 +265,14 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
@Override
|
||||
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
Optional<Model> result = findModel(model);
|
||||
if (!result.isPresent()) {
|
||||
if (result.isEmpty()) {
|
||||
LOG.info("Model {} added", model);
|
||||
recorderLock.lock();
|
||||
try {
|
||||
models.add(model);
|
||||
model.setAddedTimestamp(Instant.now());
|
||||
if (Objects.equals(model.getAddedTimestamp(), Instant.EPOCH)) {
|
||||
model.setAddedTimestamp(Instant.now());
|
||||
}
|
||||
config.getSettings().models.add(model);
|
||||
config.save();
|
||||
} catch (IOException e) {
|
||||
|
|
Loading…
Reference in New Issue