diff --git a/designs/login_credentials_shard_node_icon.svg b/designs/login_credentials_shard_node_icon.svg new file mode 100644 index 0000000..514f0c0 --- /dev/null +++ b/designs/login_credentials_shard_node_icon.svg @@ -0,0 +1,107 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/designs/text_shard_node_icon.svg b/designs/text_shard_node_icon.svg new file mode 100644 index 0000000..4a7b458 --- /dev/null +++ b/designs/text_shard_node_icon.svg @@ -0,0 +1,89 @@ + + + + + + + + + + image/svg+xml + + + + + + + + A + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index a2d95fd..797dab6 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -5,4 +5,5 @@ module crystalkeep { opens nl.andrewlalis.crystalkeep; exports nl.andrewlalis.crystalkeep.control to javafx.fxml; + exports nl.andrewlalis.crystalkeep.model to javafx.fxml; } \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java index d978a34..fdcb416 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java @@ -29,7 +29,15 @@ public class CrystalKeep extends Application { stage.setScene(scene); stage.setTitle("CrystalKeep"); stage.sizeToScene(); + model.setActiveCluster(this.loadRootCluster()); + stage.show(); + + MainViewController controller = loader.getController(); + controller.init(model); + } + + private Cluster loadRootCluster() throws IOException { ClusterLoader clusterLoader = new ClusterLoader(); Cluster rootCluster; try { @@ -45,12 +53,6 @@ public class CrystalKeep extends Application { clusterLoader.saveDefault(rootCluster); System.out.println("Saved root cluster on first load."); } - model.setActiveCluster(rootCluster); - - stage.show(); - - MainViewController controller = loader.getController(); - controller.init(model); - System.out.println(rootCluster); + return rootCluster; } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewController.java b/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewController.java deleted file mode 100644 index 0b53e57..0000000 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewController.java +++ /dev/null @@ -1,9 +0,0 @@ -package nl.andrewlalis.crystalkeep.control; - -import javafx.fxml.FXML; -import javafx.scene.control.TreeView; - -public class ClusterTreeViewController { - @FXML - TreeView clusterTreeView; -} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java index b3aca19..542d27c 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java @@ -2,8 +2,6 @@ package nl.andrewlalis.crystalkeep.control; import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.control.TextArea; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.layout.VBox; @@ -14,10 +12,21 @@ import nl.andrewlalis.crystalkeep.model.shards.TextShard; import nl.andrewlalis.crystalkeep.view.ClusterTreeItem; import nl.andrewlalis.crystalkeep.view.CrystalItemTreeCell; import nl.andrewlalis.crystalkeep.view.ShardTreeItem; +import nl.andrewlalis.crystalkeep.view.shard_details.LoginCredentialsPane; +import nl.andrewlalis.crystalkeep.view.shard_details.ShardPane; +import nl.andrewlalis.crystalkeep.view.shard_details.TextShardPane; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; public class MainViewController implements ModelListener { + private static final Map, Class>> shardPanesMap = new HashMap<>(); + static { + shardPanesMap.put(TextShard.class, TextShardPane.class); + shardPanesMap.put(LoginCredentialsShard.class, LoginCredentialsPane.class); + } + private Model model; @FXML @@ -30,17 +39,17 @@ public class MainViewController implements ModelListener { this.model.addListener(this); this.activeClusterUpdated(); assert(this.clusterTreeView != null); - this.clusterTreeView.setCellFactory(param -> new CrystalItemTreeCell()); + this.clusterTreeView.setCellFactory(param -> new CrystalItemTreeCell(this.model)); this.clusterTreeView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { shardDetailContainer.getChildren().clear(); if (newValue instanceof ShardTreeItem) { var node = (ShardTreeItem) newValue; - System.out.println(node.getShard()); - if (node.getShard() instanceof TextShard) { - shardDetailContainer.getChildren().add(new TextArea(((TextShard) node.getShard()).getText())); - } else if (node.getShard() instanceof LoginCredentialsShard) { - shardDetailContainer.getChildren().add(new Label("Username: " + ((LoginCredentialsShard) node.getShard()).getUsername())); - shardDetailContainer.getChildren().add(new Label("Password: " + ((LoginCredentialsShard) node.getShard()).getPassword())); + var paneClass = shardPanesMap.get(node.getShard().getClass()); + try { + var pane = paneClass.getDeclaredConstructor(node.getShard().getClass()).newInstance(node.getShard()); + shardDetailContainer.getChildren().add(pane); + } catch (Exception e) { + e.printStackTrace(); } } }); @@ -66,6 +75,7 @@ public class MainViewController implements ModelListener { private TreeItem createNode(Cluster cluster) { ClusterTreeItem node = new ClusterTreeItem(cluster); + node.setExpanded(true); for (Cluster child : cluster.getClustersOrdered()) { node.getChildren().add(this.createNode(child)); } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java index 4f2f313..02b721c 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java @@ -38,10 +38,18 @@ public class Cluster implements Comparable, CrystalItem { this.shards.add(shard); } + public void removeShard(Shard shard) { + this.shards.remove(shard); + } + public void addCluster(Cluster cluster) { this.clusters.add(cluster); } + public void removeCluster(Cluster cluster) { + this.clusters.remove(cluster); + } + public List getClustersOrdered() { List clusters = new ArrayList<>(this.getClusters()); clusters.sort(Comparator.naturalOrder()); diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java index cf57180..ec755c2 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java @@ -19,6 +19,10 @@ public class Model { public void setActiveCluster(Cluster activeCluster) { this.activeCluster = activeCluster; + this.notifyListeners(); + } + + public void notifyListeners() { this.listeners.forEach(ModelListener::activeClusterUpdated); } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java index f57b616..da4d004 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java @@ -1,14 +1,11 @@ package nl.andrewlalis.crystalkeep.model; -import nl.andrewlalis.crystalkeep.model.shards.ShardType; - import java.time.LocalDateTime; import java.util.Objects; /** * A shard is a single "piece" of information, such as a snippet of text, login - * credentials, an image, or a private key. All shards within a cluster should - * have unique names. + * credentials, an image, or a private key. *

* Due to the need to deserialize shards from byte arrays, it is required * that this parent class holds a type discriminator value, which is used to @@ -55,7 +52,7 @@ public abstract class Shard implements Comparable, CrystalItem { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Shard shard = (Shard) o; - return getName().equals(shard.getName()) && getType() == shard.getType(); + return getName().equals(shard.getName()) && getType() == shard.getType() && getCreatedAt().equals(shard.getCreatedAt()); } @Override diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/ShardType.java b/src/main/java/nl/andrewlalis/crystalkeep/model/ShardType.java similarity index 81% rename from src/main/java/nl/andrewlalis/crystalkeep/model/shards/ShardType.java rename to src/main/java/nl/andrewlalis/crystalkeep/model/ShardType.java index 62a8827..bdfa277 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/ShardType.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/ShardType.java @@ -1,6 +1,7 @@ -package nl.andrewlalis.crystalkeep.model.shards; +package nl.andrewlalis.crystalkeep.model; -import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; /** * Represents a distinct type of shard, and should correspond to exactly one diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java index ac451dd..1e5fe2e 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java @@ -3,7 +3,7 @@ package nl.andrewlalis.crystalkeep.model.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.shards.ShardType; +import nl.andrewlalis.crystalkeep.model.ShardType; import nl.andrewlalis.crystalkeep.model.shards.TextShard; import java.io.IOException; @@ -12,7 +12,6 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; 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 984d5eb..55ea9f9 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java @@ -1,6 +1,7 @@ package nl.andrewlalis.crystalkeep.model.shards; import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.ShardType; import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils; import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer; @@ -39,6 +40,11 @@ public class LoginCredentialsShard extends Shard { return super.toString() + ", username=\"" + this.username + "\", password=\"" + this.password + "\""; } + @Override + public String getIconPath() { + return "/nl/andrewlalis/crystalkeep/ui/images/login_credentials_shard_node_icon.png"; + } + public static class Serializer implements ShardSerializer { @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 47bfdf9..ee6d75d 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java @@ -1,6 +1,7 @@ package nl.andrewlalis.crystalkeep.model.shards; import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.ShardType; import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils; import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer; @@ -29,6 +30,11 @@ public class TextShard extends Shard { return super.toString() + ", text=\"" + this.text + "\""; } + @Override + public String getIconPath() { + return "/nl/andrewlalis/crystalkeep/ui/images/text_shard_node_icon.png"; + } + public static class Serializer implements ShardSerializer { @Override public byte[] serialize(TextShard shard) throws IOException { diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java index 859ed74..e256e35 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java @@ -5,35 +5,54 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.TreeCell; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.CrystalItem; +import nl.andrewlalis.crystalkeep.model.Model; import nl.andrewlalis.crystalkeep.model.Shard; import java.io.InputStream; public class CrystalItemTreeCell extends TreeCell { - private final ContextMenu contextMenu; + private final Model model; - public CrystalItemTreeCell() { - MenuItem item = new MenuItem("Delete"); - this.contextMenu = new ContextMenu(item); + public CrystalItemTreeCell(Model model) { + this.model = model; } + @Override protected void updateItem(CrystalItem item, boolean empty) { super.updateItem(item, empty); if (!empty) { + ContextMenu menu = new ContextMenu(); + if (item instanceof Cluster) { + var addShardItem = new MenuItem("Add Shard"); + var addClusterItem = new MenuItem("Add Cluster"); + menu.getItems().addAll(addShardItem, addClusterItem); + } + var deleteItem = new MenuItem("Delete"); + deleteItem.setOnAction(event -> { + if (this.getTreeItem().getParent() != null && this.getTreeItem().getParent() instanceof ClusterTreeItem) { + var cluster = ((ClusterTreeItem) this.getTreeItem().getParent()).getCluster(); + if (item instanceof Shard) { + cluster.removeShard((Shard) item); + } else if (item instanceof Cluster) { + cluster.removeCluster((Cluster) item); + } + this.model.notifyListeners(); + } + }); + menu.getItems().add(deleteItem); this.setText(item.getName()); InputStream is = getClass().getResourceAsStream(item.getIconPath()); if (is != null) { ImageView icon = new ImageView(new Image(is)); - icon.setFitHeight(16); + icon.setFitHeight(24); icon.setPreserveRatio(true); this.setGraphic(icon); } - if (item instanceof Shard) { - this.setContextMenu(this.contextMenu); - } + this.setContextMenu(menu); } else { this.setText(null); this.setGraphic(null); diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsPane.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsPane.java new file mode 100644 index 0000000..e9f4b8a --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsPane.java @@ -0,0 +1,56 @@ +package nl.andrewlalis.crystalkeep.view.shard_details; + +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; + +public class LoginCredentialsPane extends ShardPane { + public LoginCredentialsPane(LoginCredentialsShard shard) { + super(shard); + } + + @Override + protected Node getContent(LoginCredentialsShard shard) { + GridPane gp = new GridPane(); + gp.setPadding(new Insets(5)); + gp.setHgap(5); + gp.setVgap(5); + gp.add(new Label("Username"), 0, 0); + var usernameField = new TextField(shard.getUsername()); + usernameField.textProperty().addListener((observable, oldValue, newValue) -> { + shard.setUsername(newValue); + }); + gp.add(usernameField, 1, 0); + gp.add(new Label("Password"), 0, 1); + var passwordField = new PasswordField(); + passwordField.setText(shard.getPassword()); + var rawPasswordField = new TextField(shard.getPassword()); + rawPasswordField.setVisible(false); + passwordField.textProperty().addListener((observable, oldValue, newValue) -> { + shard.setPassword(newValue); + rawPasswordField.setText(newValue); + }); + rawPasswordField.textProperty().addListener((observable, oldValue, newValue) -> { + shard.setPassword(newValue); + passwordField.setText(newValue); + }); + var passwordsContainer = new Pane(); + passwordsContainer.getChildren().add(passwordField); + passwordsContainer.getChildren().add(rawPasswordField); + gp.add(passwordsContainer, 1, 1); + var showPasswordCheckbox = new CheckBox("Show password"); + showPasswordCheckbox.setSelected(false); + showPasswordCheckbox.selectedProperty().addListener((observable, oldValue, newValue) -> { + passwordField.setVisible(!newValue); + rawPasswordField.setVisible(newValue); + }); + gp.add(showPasswordCheckbox, 1, 2); + return gp; + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardPane.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardPane.java new file mode 100644 index 0000000..93e4a8e --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardPane.java @@ -0,0 +1,38 @@ +package nl.andrewlalis.crystalkeep.view.shard_details; + +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import nl.andrewlalis.crystalkeep.model.Shard; + +import java.time.format.DateTimeFormatter; + +public abstract class ShardPane extends VBox { + public ShardPane(T shard) { + this.setSpacing(5); + GridPane gp = new GridPane(); + gp.setPadding(new Insets(5)); + gp.setHgap(5); + gp.setVgap(5); + gp.add(new Label("Name"), 0, 0); + var nameField = new TextField(shard.getName()); + nameField.textProperty().addListener((observable, oldValue, newValue) -> { + shard.setName(newValue); + }); + gp.add(nameField, 1, 0); + gp.add(new Label("Created at"), 0, 1); + var createdAtField = new TextField(shard.getCreatedAt().format(DateTimeFormatter.ofPattern("dd MMMM yyyy HH:mm:ss"))); + createdAtField.setEditable(false); + gp.add(createdAtField, 1, 1); + this.getChildren().add(gp); + this.getChildren().add(new Separator(Orientation.HORIZONTAL)); + this.getChildren().add(this.getContent(shard)); + } + + protected abstract Node getContent(T shard); +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java new file mode 100644 index 0000000..c164dc1 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java @@ -0,0 +1,20 @@ +package nl.andrewlalis.crystalkeep.view.shard_details; + +import javafx.scene.Node; +import javafx.scene.control.TextArea; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; + +public class TextShardPane extends ShardPane { + public TextShardPane(TextShard shard) { + super(shard); + } + + @Override + protected Node getContent(TextShard shard) { + var textArea = new TextArea(shard.getText()); + textArea.textProperty().addListener((observable, oldValue, newValue) -> { + shard.setText(newValue); + }); + return textArea; + } +} diff --git a/src/main/resources/nl/andrewlalis/crystalkeep/ui/clusters_view.fxml b/src/main/resources/nl/andrewlalis/crystalkeep/ui/clusters_view.fxml deleted file mode 100644 index 93bb4c6..0000000 --- a/src/main/resources/nl/andrewlalis/crystalkeep/ui/clusters_view.fxml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - -

- -
- 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 new file mode 100644 index 0000000..ba169d3 Binary files /dev/null and b/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/login_credentials_shard_node_icon.png differ diff --git a/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/text_shard_node_icon.png b/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/text_shard_node_icon.png new file mode 100644 index 0000000..c623b85 Binary files /dev/null and b/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/text_shard_node_icon.png differ