From f04eb5310e8618efc34dbb898e7241f8ed7cc965 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Mon, 20 Dec 2021 15:57:50 +0100 Subject: [PATCH] Add export and import function for the model lists (recording and later) --- .../ui/controls/AbstractFileSelectionBox.java | 29 ++- .../main/java/ctbrec/ui/controls/Dialogs.java | 58 +----- .../ctbrec/ui/controls/FileSelectionBox.java | 2 +- .../recorded/AbstractRecordedModelsTab.java | 103 ++++++++-- .../ui/tabs/recorded/ModelExportDialog.java | 84 ++++++++ .../ui/tabs/recorded/ModelImportExport.java | 184 ++++++++++++++++++ .../ui/tabs/recorded/PortraitStore.java | 47 +++++ .../ui/tabs/recorded/RecordLaterTab.java | 30 +-- .../recorded/RecordedModelsPerSiteTab.java | 24 ++- .../ui/tabs/recorded/RecordedModelsTab.java | 5 + client/src/main/resources/16/download.png | Bin 0 -> 4753 bytes client/src/main/resources/16/upload.png | Bin 0 -> 4761 bytes client/src/main/resources/32/download.png | Bin 0 -> 354 bytes client/src/main/resources/32/upload.png | Bin 0 -> 349 bytes .../ctbrec/recorder/NextGenLocalRecorder.java | 6 +- 15 files changed, 467 insertions(+), 105 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/tabs/recorded/ModelExportDialog.java create mode 100644 client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java create mode 100644 client/src/main/java/ctbrec/ui/tabs/recorded/PortraitStore.java create mode 100644 client/src/main/resources/16/download.png create mode 100644 client/src/main/resources/16/upload.png create mode 100644 client/src/main/resources/32/download.png create mode 100644 client/src/main/resources/32/upload.png diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java index 5c6477cd..357031f9 100644 --- a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -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()); diff --git a/client/src/main/java/ctbrec/ui/controls/Dialogs.java b/client/src/main/java/ctbrec/ui/controls/Dialogs.java index b3e68658..340cfca0 100644 --- a/client/src/main/java/ctbrec/ui/controls/Dialogs.java +++ b/client/src/main/java/ctbrec/ui/controls/Dialogs.java @@ -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 showModelGroupSelectionDialog(Scene parent, Model model) { - var dialogPane = new GridPane(); - Set modelGroups = Config.getInstance().getSettings().modelGroups; - ObservableList comboBoxModel = FXCollections.observableArrayList(modelGroups); - ComboBox 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 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(); - } - } } diff --git a/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java index 684f1c3a..caf3efd5 100644 --- a/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java @@ -12,7 +12,7 @@ public class FileSelectionBox extends AbstractFileSelectionBox { @Override protected String validate(File file) { - if (isDisabled()) { + if (isDisabled() || validationDisabled) { return null; } diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java index d2cf13a2..94324e9b 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java @@ -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 portraitCache = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.DAYS) .maximumSize(1000) - .build(CacheLoader.from(AbstractRecordedModelsTab::loadModelPortrait)); + .build(CacheLoader.from(this::loadModelPortrait)); protected AutoFillTextField modelInputField; protected List 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 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 models) { + getContent().setCursor(Cursor.WAIT); + Task 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 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 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 selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); + List 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); } } diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelExportDialog.java b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelExportDialog.java new file mode 100644 index 00000000..95e7463a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelExportDialog.java @@ -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 = 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); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java new file mode 100644 index 00000000..87b50fc7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java @@ -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 includes, File targetFile) { + } + + private ModelImportExport() { + } + + public static void exportTo(List 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> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class)); + JsonAdapter> modelListAdapter = moshi.adapter(Types.newParameterizedType(List.class, Model.class)); + JsonAdapter> 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 entry : config.getSettings().modelPortraits.entrySet()) { + String modelUrl = entry.getKey(); + String portraitId = entry.getValue(); + Optional 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 importFrom(File target, List 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 modelAdapter = moshi.adapter(Model.class); + JsonAdapter> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class)); + JsonAdapter> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class)); + + List 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> 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> notesAdapter, Config config) throws IOException { + var notes = notesAdapter.fromJson(reader); + if (notes != null) { + config.getSettings().modelNotes.putAll(notes); + } + } + + private static List readModels(JsonAdapter modelAdapter, JsonReader reader) throws IOException { + List 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; + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/PortraitStore.java b/client/src/main/java/ctbrec/ui/tabs/recorded/PortraitStore.java new file mode 100644 index 00000000..596f94af --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/PortraitStore.java @@ -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 loadModelPortrait(String modelUrl) { + return loadModelPortraitFile(modelUrl).map(bytes -> new Image(new ByteArrayInputStream(bytes))); + } + + public Optional 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); + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java index 7d70bf3f..c7ac7632 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java @@ -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 getExportList() { + return recorder.getModels().stream().filter(Model::isMarkedForLaterRecording).toList(); + } + void initializeUpdateService() { updateService = createUpdateService(); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2))); diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsPerSiteTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsPerSiteTab.java index b4aa037b..a68acad4 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsPerSiteTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsPerSiteTab.java @@ -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 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 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 getExportList() { + return getFilteredModelsForTab(); + } + + private List getFilteredModelsForTab() { + return recorder.getModels().stream() + .filter(Predicate.not(Model::isMarkedForLaterRecording)) + .filter(m -> Objects.equals(m.getSite(), sites.get(0))) + .toList(); + } } 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 8f9ded8c..21a45b16 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -169,6 +169,11 @@ public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabS restoreState(); } + @Override + protected List getExportList() { + return recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).toList(); + } + private void onUpdatePriority(CellEditEvent evt) { try { int prio = Optional.ofNullable(evt.getNewValue()).map(Number::intValue).orElse(-1); diff --git a/client/src/main/resources/16/download.png b/client/src/main/resources/16/download.png new file mode 100644 index 0000000000000000000000000000000000000000..673aa322692b4b28098d1bee499e9d83461d0c09 GIT binary patch literal 4753 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 literal 0 HcmV?d00001 diff --git a/client/src/main/resources/32/download.png b/client/src/main/resources/32/download.png new file mode 100644 index 0000000000000000000000000000000000000000..717cd15b6eba171a7c2fd1d44b13803e2a11601b GIT binary patch literal 354 zcmV-o0iFJdP)(&@4)xTk3O7!)#R3O_%xBG>1Atd`03ARF z&;i&0cEFqJc3Y>aGjCw40oVY;24PJ#1lF1ZGa$zP)6qHxrdsMLaP<&=0~XqFUl2cq z{|ezH+!exWxEA6DEPaPJg=jUr4#GLyHszN61R^<8?h`REmwS=yt*ci|(M`f_5ErpF z+&Dq8+4x1?s>{-b8#^_a7qAD$;)7gM*9r{9ztrYD@DSV9g~P$zlGZ7aZfC3fHWkZR#rgh8DJ!~tv|r|3%*1ZZvjT&MF0Q*07*qoM6N<$f-g&v A3jhEB literal 0 HcmV?d00001 diff --git a/client/src/main/resources/32/upload.png b/client/src/main/resources/32/upload.png new file mode 100644 index 0000000000000000000000000000000000000000..417d7859798492bd144d27ec41808e74963b4613 GIT binary patch literal 349 zcmV-j0iyniP)K~z|U?UqXl!Y~v?PZcUUaRq*G4Wb(qao`@rJ?H{lgHA=Pdl2I! z4t#N-jZNCLo%F(^ge3Q#gr+Iv*BCefM_^RICzpo68PHN!U{Y6jr4Y98N+7brONDR^ zF9pIkJSaro@E{OH!wW$SflGjJQy1}c);3_csZCl?%K~r!?momBx{K$Jq8w&Aqu*|P&p&=jo*f521uV@Zm&;@-=1a>Q1uSID%+d3K<3J7U z{;d34dkG%u{favb8@7{pX{B1n5d7DeceL+P?!4@8gs9 vQWcfi?t!HZpxQtWm;>#=0DUQ2`w6UH 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) {