From a9c335dccadec624e045e9451728de1438dbb5e3 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 3 Jun 2021 09:42:23 +0200 Subject: [PATCH] Added File shard, improved shard icons. --- designs/login_credentials_shard_node_icon.svg | 107 ------------------ designs/shard_node_icon.svg | 36 +++--- designs/shards/file.svg | 78 +++++++++++++ designs/shards/login_credentials.svg | 95 ++++++++++++++++ .../text.svg} | 47 +++----- .../crystalkeep/control/AddShardHandler.java | 2 +- .../ClusterTreeViewItemSelectionListener.java | 2 +- .../control/MainViewController.java | 55 ++++----- .../io/serialization/ByteUtils.java | 33 +++++- .../io/serialization/ClusterSerializer.java | 6 +- .../io/serialization/ShardSerializer.java | 8 +- .../crystalkeep/model/shards/FileShard.java | 36 ++++-- .../model/shards/LoginCredentialsShard.java | 11 +- .../crystalkeep/model/shards/TextShard.java | 11 +- .../crystalkeep/util/StringUtils.java | 10 ++ .../crystalkeep/view/CrystalItemTreeCell.java | 2 +- .../view/shards/FileShardViewModel.java | 92 +++++++++++++++ .../LoginCredentialsViewModel.java | 2 +- .../ShardViewModel.java | 2 +- .../TextShardViewModel.java | 2 +- .../{shard_details => shards}/ViewModels.java | 4 +- .../crystalkeep/ui/crystalkeep.fxml | 2 +- .../ui/images/file_shard_node_icon.png | Bin 0 -> 1757 bytes .../login_credentials_shard_node_icon.png | Bin 1049 -> 3526 bytes .../crystalkeep/ui/images/shard_node_icon.png | Bin 795 -> 5411 bytes .../ui/images/text_shard_node_icon.png | Bin 1096 -> 2359 bytes 26 files changed, 417 insertions(+), 226 deletions(-) delete mode 100644 designs/login_credentials_shard_node_icon.svg create mode 100644 designs/shards/file.svg create mode 100644 designs/shards/login_credentials.svg rename designs/{text_shard_node_icon.svg => shards/text.svg} (52%) create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/util/StringUtils.java create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/view/shards/FileShardViewModel.java rename src/main/java/nl/andrewlalis/crystalkeep/view/{shard_details => shards}/LoginCredentialsViewModel.java (98%) rename src/main/java/nl/andrewlalis/crystalkeep/view/{shard_details => shards}/ShardViewModel.java (96%) rename src/main/java/nl/andrewlalis/crystalkeep/view/{shard_details => shards}/TextShardViewModel.java (92%) rename src/main/java/nl/andrewlalis/crystalkeep/view/{shard_details => shards}/ViewModels.java (86%) create mode 100644 src/main/resources/nl/andrewlalis/crystalkeep/ui/images/file_shard_node_icon.png diff --git a/designs/login_credentials_shard_node_icon.svg b/designs/login_credentials_shard_node_icon.svg deleted file mode 100644 index 514f0c0..0000000 --- a/designs/login_credentials_shard_node_icon.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - diff --git a/designs/shard_node_icon.svg b/designs/shard_node_icon.svg index 24fc994..c5e7ae4 100644 --- a/designs/shard_node_icon.svg +++ b/designs/shard_node_icon.svg @@ -16,9 +16,9 @@ id="svg8" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="shard_node_icon.svg" - inkscape:export-filename="A:\Programming\GitHub-andrewlalis\CrystalKeep\src\main\resources\ui\images\shard_node_icon.png" - inkscape:export-xdpi="192" - inkscape:export-ydpi="192"> + inkscape:export-filename="A:\Programming\GitHub-andrewlalis\CrystalKeep\src\main\resources\nl\andrewlalis\crystalkeep\ui\images\shard_node_icon.png" + inkscape:export-xdpi="1536" + inkscape:export-ydpi="1536"> + units="px" + showguides="true" + inkscape:guide-bbox="true" /> @@ -58,21 +60,9 @@ id="layer1" transform="translate(0,-292.76667)"> + style="fill:#00f8c5;stroke:none;stroke-width:0.26458333;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1" + d="m 2.1143043,292.83045 1.1824699,2.0481 -1.1824699,2.0481 -1.18246994,-2.0481 1.18246994,-2.0481" + id="path820" + inkscape:connector-curvature="0" /> diff --git a/designs/shards/file.svg b/designs/shards/file.svg new file mode 100644 index 0000000..2ee1ef1 --- /dev/null +++ b/designs/shards/file.svg @@ -0,0 +1,78 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/designs/shards/login_credentials.svg b/designs/shards/login_credentials.svg new file mode 100644 index 0000000..f2fcb90 --- /dev/null +++ b/designs/shards/login_credentials.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/designs/text_shard_node_icon.svg b/designs/shards/text.svg similarity index 52% rename from designs/text_shard_node_icon.svg rename to designs/shards/text.svg index 4a7b458..e9e50f1 100644 --- a/designs/text_shard_node_icon.svg +++ b/designs/shards/text.svg @@ -15,10 +15,10 @@ version="1.1" id="svg8" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" - sodipodi:docname="text_shard_node_icon.svg" + sodipodi:docname="text.svg" inkscape:export-filename="A:\Programming\GitHub-andrewlalis\CrystalKeep\src\main\resources\nl\andrewlalis\crystalkeep\ui\images\text_shard_node_icon.png" - inkscape:export-xdpi="192" - inkscape:export-ydpi="192"> + inkscape:export-xdpi="1536" + inkscape:export-ydpi="1536"> + units="px" + showguides="true" + inkscape:guide-bbox="true" /> @@ -57,33 +59,16 @@ inkscape:groupmode="layer" id="layer1" transform="translate(0,-292.76667)"> - A + id="tspan815" + x="0.36164278" + y="296.76907" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.69617891px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.35601118">T diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/AddShardHandler.java b/src/main/java/nl/andrewlalis/crystalkeep/control/AddShardHandler.java index 528a24c..b15575b 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/AddShardHandler.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/AddShardHandler.java @@ -53,7 +53,7 @@ public class AddShardHandler implements EventHandler { switch (type) { case TEXT: return new TextShard(name); case LOGIN_CREDENTIALS: return new LoginCredentialsShard(name); - case FILE: return new FileShard(name, "", "", new byte[0]); + case FILE: return new FileShard(name, "", new byte[0]); default: return null; } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java b/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java index eee88e9..9c4f503 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java @@ -6,7 +6,7 @@ import javafx.scene.control.TreeItem; import javafx.scene.layout.VBox; import nl.andrewlalis.crystalkeep.model.CrystalItem; import nl.andrewlalis.crystalkeep.view.ShardTreeItem; -import nl.andrewlalis.crystalkeep.view.shard_details.ViewModels; +import nl.andrewlalis.crystalkeep.view.shards.ViewModels; /** * This listener will update the shard detail container pane (the main center diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java index bb1ae55..48c7eea 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java @@ -23,6 +23,8 @@ public class MainViewController implements ModelListener { public TreeView clusterTreeView; @FXML public VBox shardDetailContainer; + @FXML + public Menu fileMenu; public void init(Model model) { this.model = model; @@ -91,7 +93,6 @@ public class MainViewController implements ModelListener { @FXML public void save() { if (model.getActiveCluster() == null) return; - ClusterIO loader = new ClusterIO(); Path path = model.getActiveClusterPath(); if (path == null) { FileChooser chooser = new FileChooser(); @@ -102,27 +103,12 @@ public class MainViewController implements ModelListener { if (file == null) return; path = file.toPath(); } - char[] pw = this.model.getActiveClusterPassword(); - if (pw == null) { - pw = this.promptPassword().orElse(new char[0]); - } - try { - if (pw.length == 0) { - loader.saveUnencrypted(model.getActiveCluster(), path); - } else { - loader.save(model.getActiveCluster(), path, pw); - } - } catch (Exception e) { - e.printStackTrace(); - var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster."); - alert.showAndWait(); - } + this.saveCluster(path); } @FXML public void saveAs() { if (model.getActiveCluster() == null) return; - ClusterIO clusterIO = new ClusterIO(); Path path = model.getActiveClusterPath(); FileChooser chooser = new FileChooser(); chooser.setTitle("Save Cluster"); @@ -133,21 +119,7 @@ public class MainViewController implements ModelListener { File file = chooser.showSaveDialog(this.clusterTreeView.getScene().getWindow()); if (file == null) return; path = file.toPath(); - char[] pw = this.model.getActiveClusterPassword(); - if (pw == null) { - pw = this.promptPassword().orElse(new char[0]); - } - try { - if (pw.length == 0) { - clusterIO.saveUnencrypted(model.getActiveCluster(), path); - } else { - clusterIO.save(model.getActiveCluster(), path, pw); - } - } catch (Exception e) { - e.printStackTrace(); - var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster."); - alert.showAndWait(); - } + this.saveCluster(path); } @FXML @@ -175,4 +147,23 @@ public class MainViewController implements ModelListener { }); return d.showAndWait(); } + + private void saveCluster(Path path) { + char[] pw = this.model.getActiveClusterPassword(); + if (pw == null) { + pw = this.promptPassword().orElse(new char[0]); + } + ClusterIO loader = new ClusterIO(); + try { + if (pw.length == 0) { + loader.saveUnencrypted(model.getActiveCluster(), path); + } else { + loader.save(model.getActiveCluster(), path, pw); + } + } catch (Exception e) { + e.printStackTrace(); + var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster."); + alert.showAndWait(); + } + } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ByteUtils.java b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ByteUtils.java index 5d4f0a6..97cfec5 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ByteUtils.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ByteUtils.java @@ -1,6 +1,5 @@ package nl.andrewlalis.crystalkeep.io.serialization; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -25,6 +24,32 @@ public class ByteUtils { os.write(toBytes(value)); } + public static byte[] longToBytes(long l) { + byte[] result = new byte[8]; + for (int i = 7; i >= 0; i--) { + result[i] = (byte)(l & 0xFF); + l >>= 8; + } + return result; + } + + public static long bytesToLong(final byte[] b) { + long result = 0; + for (int i = 0; i < 8; i++) { + result <<= 8; + result |= (b[i] & 0xFF); + } + return result; + } + + public static void writeLong(long value, OutputStream os) throws IOException { + os.write(longToBytes(value)); + } + + public static long readLong(InputStream is) throws IOException { + return bytesToLong(is.readNBytes(Long.BYTES)); + } + public static void writeLengthPrefixed(byte[] bytes, OutputStream os) throws IOException { os.write(toBytes(bytes.length)); os.write(bytes); @@ -34,12 +59,10 @@ public class ByteUtils { writeLengthPrefixed(s.getBytes(StandardCharsets.UTF_8), os); } - public static byte[] writeLengthPrefixedStrings(String[] strings) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + public static void writeLengthPrefixedStrings(String[] strings, OutputStream os) throws IOException { for (String s : strings) { - writeLengthPrefixed(s, bos); + writeLengthPrefixed(s, os); } - return bos.toByteArray(); } public static byte[] readLengthPrefixed(InputStream is) throws IOException { diff --git a/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializer.java index 8603034..7745c6b 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializer.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializer.java @@ -2,8 +2,9 @@ package nl.andrewlalis.crystalkeep.io.serialization; import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.Shard; -import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; import nl.andrewlalis.crystalkeep.model.ShardType; +import nl.andrewlalis.crystalkeep.model.shards.FileShard; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; import nl.andrewlalis.crystalkeep.model.shards.TextShard; import java.io.IOException; @@ -27,6 +28,7 @@ public class ClusterSerializer { static { serializers.put(ShardType.TEXT, new TextShard.Serializer()); serializers.put(ShardType.LOGIN_CREDENTIALS, new LoginCredentialsShard.Serializer()); + serializers.put(ShardType.FILE, new FileShard.Serializer()); } /** @@ -82,7 +84,7 @@ public class ClusterSerializer { ByteUtils.writeLengthPrefixed(shard.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).getBytes(StandardCharsets.UTF_8), os); os.write(ByteUtils.toBytes(shard.getType().getValue())); ShardSerializer serializer = serializers.get(shard.getType()); - os.write(serializer.serialize(shard)); + serializer.serialize(shard, os); } /** diff --git a/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ShardSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ShardSerializer.java index 1106aec..9dc2496 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ShardSerializer.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ShardSerializer.java @@ -4,10 +4,16 @@ import nl.andrewlalis.crystalkeep.model.Shard; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.time.LocalDateTime; +/** + * This interface should be implemented to provide the ability to serialize and + * deserialize a shard to and from a stream of bytes. + * @param The shard type. + */ public interface ShardSerializer { - byte[] serialize(T shard) throws IOException; + void serialize(T shard, OutputStream os) throws IOException; T deserialize(InputStream is, String name, LocalDateTime createdAt) throws IOException; } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/FileShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/FileShard.java index ebd8d56..ba15c5e 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/FileShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/FileShard.java @@ -1,35 +1,57 @@ package nl.andrewlalis.crystalkeep.model.shards; +import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils; +import nl.andrewlalis.crystalkeep.io.serialization.ShardSerializer; import nl.andrewlalis.crystalkeep.model.Shard; import nl.andrewlalis.crystalkeep.model.ShardType; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.time.LocalDateTime; public class FileShard extends Shard { private String fileName; - private String mimeType; private byte[] contents; - public FileShard(String name, LocalDateTime createdAt, String fileName, String mimeType, byte[] contents) { + public FileShard(String name, LocalDateTime createdAt, String fileName, byte[] contents) { super(name, createdAt, ShardType.FILE); this.fileName = fileName; - this.mimeType = mimeType; this.contents = contents; } - public FileShard(String name, String fileName, String mimeType, byte[] contents) { - this(name, LocalDateTime.now(), fileName, mimeType, contents); + public FileShard(String name, String fileName, byte[] contents) { + this(name, LocalDateTime.now(), fileName, contents); } public String getFileName() { return fileName; } - public String getMimeType() { - return mimeType; + public void setFileName(String fileName) { + this.fileName = fileName; } public byte[] getContents() { return contents; } + + public void setContents(byte[] contents) { + this.contents = contents; + } + + public static final class Serializer implements ShardSerializer { + @Override + public void serialize(FileShard shard, OutputStream os) throws IOException { + ByteUtils.writeLengthPrefixed(shard.getFileName(), os); + ByteUtils.writeLengthPrefixed(shard.getContents(), os); + } + + @Override + public FileShard deserialize(InputStream is, String name, LocalDateTime createdAt) throws IOException { + String fileName = ByteUtils.readLengthPrefixedString(is); + byte[] contents = ByteUtils.readLengthPrefixed(is); + return new FileShard(name, createdAt, fileName, contents); + } + } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java index 7d7ba75..7e2103f 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java @@ -1,12 +1,13 @@ package nl.andrewlalis.crystalkeep.model.shards; -import nl.andrewlalis.crystalkeep.model.Shard; -import nl.andrewlalis.crystalkeep.model.ShardType; import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils; import nl.andrewlalis.crystalkeep.io.serialization.ShardSerializer; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.ShardType; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.time.LocalDateTime; public class LoginCredentialsShard extends Shard { @@ -44,11 +45,11 @@ public class LoginCredentialsShard extends Shard { return super.toString() + ", username=\"" + this.username + "\", password=\"" + this.password + "\""; } - public static class Serializer implements ShardSerializer { + public static final class Serializer implements ShardSerializer { @Override - public byte[] serialize(LoginCredentialsShard shard) throws IOException { - return ByteUtils.writeLengthPrefixedStrings(new String[]{shard.getUsername(), shard.getPassword()}); + public void serialize(LoginCredentialsShard shard, OutputStream os) throws IOException { + ByteUtils.writeLengthPrefixedStrings(new String[]{shard.getUsername(), shard.getPassword()}, os); } @Override diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java index 328d132..44e5fed 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java @@ -1,12 +1,13 @@ package nl.andrewlalis.crystalkeep.model.shards; -import nl.andrewlalis.crystalkeep.model.Shard; -import nl.andrewlalis.crystalkeep.model.ShardType; import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils; import nl.andrewlalis.crystalkeep.io.serialization.ShardSerializer; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.ShardType; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.time.LocalDateTime; public class TextShard extends Shard { @@ -34,10 +35,10 @@ public class TextShard extends Shard { return super.toString() + ", text=\"" + this.text + "\""; } - public static class Serializer implements ShardSerializer { + public static final class Serializer implements ShardSerializer { @Override - public byte[] serialize(TextShard shard) throws IOException { - return ByteUtils.writeLengthPrefixedStrings(new String[]{shard.getText()}); + public void serialize(TextShard shard, OutputStream os) throws IOException { + ByteUtils.writeLengthPrefixedStrings(new String[]{shard.getText()}, os); } @Override diff --git a/src/main/java/nl/andrewlalis/crystalkeep/util/StringUtils.java b/src/main/java/nl/andrewlalis/crystalkeep/util/StringUtils.java new file mode 100644 index 0000000..66d3d50 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/util/StringUtils.java @@ -0,0 +1,10 @@ +package nl.andrewlalis.crystalkeep.util; + +public class StringUtils { + public static boolean endsWithAny(String s, String... suffixes) { + for (String suffix : suffixes) { + if (s.endsWith(suffix)) return true; + } + return false; + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java index 8bafa6e..3b377a2 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java @@ -12,7 +12,7 @@ import nl.andrewlalis.crystalkeep.model.CrystalItem; import nl.andrewlalis.crystalkeep.model.Model; import nl.andrewlalis.crystalkeep.model.Shard; import nl.andrewlalis.crystalkeep.util.ImageCache; -import nl.andrewlalis.crystalkeep.view.shard_details.ViewModels; +import nl.andrewlalis.crystalkeep.view.shards.ViewModels; public class CrystalItemTreeCell extends TreeCell { private static final String CLUSTER_ICON = "/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png"; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shards/FileShardViewModel.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/FileShardViewModel.java new file mode 100644 index 0000000..51066bf --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/FileShardViewModel.java @@ -0,0 +1,92 @@ +package nl.andrewlalis.crystalkeep.view.shards; + +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import nl.andrewlalis.crystalkeep.model.shards.FileShard; +import nl.andrewlalis.crystalkeep.util.StringUtils; + +import java.io.*; + +public class FileShardViewModel extends ShardViewModel { + public FileShardViewModel(FileShard shard) { + super(shard); + } + + @Override + protected Node getContent(FileShard shard) { + VBox container = new VBox(10); + container.setPadding(new Insets(5)); + GridPane gp = new GridPane(); + gp.setVgap(5); + gp.setHgap(5); + + gp.add(new Label("File Name"), 0, 0); + TextField nameField = new TextField(shard.getFileName()); + nameField.textProperty().addListener((observable, oldValue, newValue) -> { + shard.setFileName(newValue); + }); + gp.add(nameField, 1, 0); + gp.add(new Label("File Size"), 0, 1); + TextField sizeField = new TextField(shard.getContents().length + " bytes"); + sizeField.setEditable(false); + gp.add(sizeField, 1, 1); + + Button setFileButton = new Button("Set File"); + setFileButton.setOnAction(event -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose a File"); + File file = chooser.showOpenDialog(gp.getScene().getWindow()); + if (file != null) { + try (FileInputStream fis = new FileInputStream(file)) { + byte[] contents = fis.readAllBytes(); + shard.setFileName(file.getName()); + shard.setContents(contents); + nameField.setText(shard.getFileName()); + sizeField.setText(shard.getContents().length + " bytes"); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + gp.add(setFileButton, 0, 2); + + Button extractFileButton = new Button("Extract File"); + extractFileButton.setOnAction(event -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Extract File"); + chooser.setInitialFileName(shard.getFileName()); + File file = chooser.showSaveDialog(gp.getScene().getWindow()); + if (file != null) { + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(shard.getContents()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + gp.add(extractFileButton, 1, 2); + container.getChildren().add(gp); + + if (StringUtils.endsWithAny(shard.getFileName().toLowerCase(), ".png", ".jpg", ".jpeg", ".gif")) { + container.getChildren().add(new Separator(Orientation.HORIZONTAL)); + ImageView imageView = new ImageView(new Image(new ByteArrayInputStream(shard.getContents()))); + imageView.setPreserveRatio(true); + ScrollPane scrollPane = new ScrollPane(imageView); + container.getChildren().add(scrollPane); + } + + return container; + } + + @Override + public String getIconPath() { + return "/nl/andrewlalis/crystalkeep/ui/images/file_shard_node_icon.png"; + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/LoginCredentialsViewModel.java similarity index 98% rename from src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java rename to src/main/java/nl/andrewlalis/crystalkeep/view/shards/LoginCredentialsViewModel.java index b23cfb1..cae0a50 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/LoginCredentialsViewModel.java @@ -1,4 +1,4 @@ -package nl.andrewlalis.crystalkeep.view.shard_details; +package nl.andrewlalis.crystalkeep.view.shards; import javafx.geometry.Insets; import javafx.scene.Node; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardViewModel.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/ShardViewModel.java similarity index 96% rename from src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardViewModel.java rename to src/main/java/nl/andrewlalis/crystalkeep/view/shards/ShardViewModel.java index 7c7e194..8826c0a 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardViewModel.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/ShardViewModel.java @@ -1,4 +1,4 @@ -package nl.andrewlalis.crystalkeep.view.shard_details; +package nl.andrewlalis.crystalkeep.view.shards; import javafx.geometry.Insets; import javafx.geometry.Orientation; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardViewModel.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/TextShardViewModel.java similarity index 92% rename from src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardViewModel.java rename to src/main/java/nl/andrewlalis/crystalkeep/view/shards/TextShardViewModel.java index 97ffc9a..c134fd0 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardViewModel.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/TextShardViewModel.java @@ -1,4 +1,4 @@ -package nl.andrewlalis.crystalkeep.view.shard_details; +package nl.andrewlalis.crystalkeep.view.shards; import javafx.scene.Node; import javafx.scene.control.TextArea; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ViewModels.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/ViewModels.java similarity index 86% rename from src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ViewModels.java rename to src/main/java/nl/andrewlalis/crystalkeep/view/shards/ViewModels.java index 09f4581..514f2a1 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ViewModels.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shards/ViewModels.java @@ -1,6 +1,7 @@ -package nl.andrewlalis.crystalkeep.view.shard_details; +package nl.andrewlalis.crystalkeep.view.shards; import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.shards.FileShard; import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; import nl.andrewlalis.crystalkeep.model.shards.TextShard; @@ -17,6 +18,7 @@ public class ViewModels { static { shardViewModels.put(TextShard.class, TextShardViewModel.class); shardViewModels.put(LoginCredentialsShard.class, LoginCredentialsViewModel.class); + shardViewModels.put(FileShard.class, FileShardViewModel.class); } public static Optional> get(Shard shard) { diff --git a/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml b/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml index cbc72e8..18ffd1c 100644 --- a/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml +++ b/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml @@ -4,7 +4,7 @@ - + diff --git a/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/file_shard_node_icon.png b/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/file_shard_node_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f08919bbc4b70fece9dbeb7f26337be6b2a3a056 GIT binary patch literal 1757 zcmeHI`BPI@6ut?Rb!5{*P)ihJDag>q1q2Boo4iV>h_VPmFaoA5K?oBtY;DI;0Rf){ z3`>ws0ci{9w6csWVT3?5A+i+8V#E^i28kgALJAGg2mTBF(QoF?S?Iq%YLy@U>^)?#oC;Nq^0w6oNeK6OL zyg(gb$?yqf1kqy|%;=ODz+^HVXi4#@)ac|G2YO0eh1_*706Lc-@6#93E8oBF$-f+j z;k{MA6ntg3i+yE?LTofIvHtX+->;NFee2V--%eSW8ie8U@=e&})|(Vty;{!$y(UgK zEr-c@rH_bd8m-Ajy~B+8pC%Xwjrx5?lDvn8#mCfLA}x#96G%*(M#^Ec2Fa+d9pu(J zNv#y7E)^2P)S?+LwTR$e&FcLnbPi~U{8q%O=aw7~IU-h1?2LpYu=&jb3ALjZbgT@Y&3M;P zf0R5@#>3^@*;>t{V7bF#;o&c9&z7agr`!EU7zqVS2qB8i8b4|VN{4ZWMDAOe4iRm| zv~o8RudPI~kh*xu5z9S4B^D1Fe%Vn>2&Wdjv2i9Y|6FSg>^tkLvt2@(BKS34x7^%# z>4*8oi1-hC?c9>&^_6*4SK(b0L|y<~E$F0K>VYG(qYXhBYjb7&QmM3t&>f)sz1`_E z2A2D^bB6M2WR~jB>;EX;64qiOPOe1hgCodNKVBrPaO%%ezb+$m`=vxnA>C5&7%s%S zbH*cCiPJe-8$s}ULU$hWZ}*Z`bPP$Ocs?;Twa=Ke^6E!PJ(PVPZtdP`MkTGz{9qhQ zr_)`P1BW2xEMXkZOb}nxy#*dC3#~}y;jKw?Px`IS!g#`3BK&;0$pxpKYoHdh=t8UkCUtSQ8l)F3q*WE7xQpDZ50jAnzg%#>LwpE@Gg-~NBS(H1i0A&+^jFWIM_)G z)CO=RiRBp59GSJq!H(b8T$7XYXAPC0+FM#$Ud;;oKJa`F z&iR9f4}b>|lY0dV`m{lw{!W397av>l{|-Z=h(s~1qrIp)#YESt8M}@q*Eb9_&7Eqc z$Hm9T%Po^D9KXmcHO#<8|DkWYYjlPBf- F{{YKfuU-HE literal 0 HcmV?d00001 diff --git a/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/login_credentials_shard_node_icon.png b/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/login_credentials_shard_node_icon.png index ba169d31161353077a3e554b3dcfa1e7660f7fce..5940c2f477fcc13bdc0b9f3032305026fbb3c5b7 100644 GIT binary patch literal 3526 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 VOTnnwkNW@s002ovPDHLkV1k^9@wos1