From 5ab1abbfff7ce49a65b58d7b772e6febe72b5606 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sat, 29 May 2021 23:09:39 +0200 Subject: [PATCH] Added ability to edit and prepare nicer views for shards. --- designs/login_credentials_shard_node_icon.svg | 107 ++++++++++++++++++ designs/text_shard_node_icon.svg | 89 +++++++++++++++ src/main/java/module-info.java | 1 + .../andrewlalis/crystalkeep/CrystalKeep.java | 16 +-- .../control/ClusterTreeViewController.java | 9 -- .../control/MainViewController.java | 28 +++-- .../crystalkeep/model/Cluster.java | 8 ++ .../andrewlalis/crystalkeep/model/Model.java | 4 + .../andrewlalis/crystalkeep/model/Shard.java | 7 +- .../model/{shards => }/ShardType.java | 5 +- .../serialization/ClusterSerializer.java | 3 +- .../model/shards/LoginCredentialsShard.java | 6 + .../crystalkeep/model/shards/TextShard.java | 6 + .../crystalkeep/view/CrystalItemTreeCell.java | 35 ++++-- .../shard_details/LoginCredentialsPane.java | 56 +++++++++ .../view/shard_details/ShardPane.java | 38 +++++++ .../view/shard_details/TextShardPane.java | 20 ++++ .../crystalkeep/ui/clusters_view.fxml | 18 --- .../login_credentials_shard_node_icon.png | Bin 0 -> 1049 bytes .../ui/images/text_shard_node_icon.png | Bin 0 -> 1096 bytes 20 files changed, 396 insertions(+), 60 deletions(-) create mode 100644 designs/login_credentials_shard_node_icon.svg create mode 100644 designs/text_shard_node_icon.svg delete mode 100644 src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewController.java rename src/main/java/nl/andrewlalis/crystalkeep/model/{shards => }/ShardType.java (81%) create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsPane.java create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardPane.java create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java delete mode 100644 src/main/resources/nl/andrewlalis/crystalkeep/ui/clusters_view.fxml create mode 100644 src/main/resources/nl/andrewlalis/crystalkeep/ui/images/login_credentials_shard_node_icon.png create mode 100644 src/main/resources/nl/andrewlalis/crystalkeep/ui/images/text_shard_node_icon.png 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 0000000000000000000000000000000000000000..ba169d31161353077a3e554b3dcfa1e7660f7fce GIT binary patch literal 1049 zcmV+!1m^pRP)%K|5(;~Ls>je#Iyc8nMi_(Z10^UMPg+6$JHfZ8QeW-2BPC}%n6kC%f zo6GFX@uAx^nVlJSllayT24>DV|L^u9>w50 zlop@zR?|`gmwId-1>yh-9bp=w1_TD?J;o8Xplk^!zX|0NpP8Onv;h?cME!Fky+1JY z(@~t6X)3BWQ1w_NMkJrk(?oFw41)o6^5eY+-sJfEuP%t4Inqu2$6w{ux<f$Ly)Uf$bglN#1yTx1d|Z5jYmaRxAX1{KO&P zMbxMo!Qu_SoNe1QG&B@tK;i9VpMG2L1S*Gk4p`@xi^XE7s_zwx#ezIkU0=>NzeL^y zazCXn1u@ySO>=W|U@j2KYr9AFv(^fk3^gD&CQuo~U=XKv^z)1Ii1DN9xL-{V^S?a$Wh&2>gT40{jc$!ni07aLz}sFqmoW0X7#>E6}q) zA@zmS7W}VRgKAWY#VBTV2ruEdWv~CZ{KVzo#_y0v4VAp9~y&N^yFdzQ`*Dzjn TQ1N)d00000NkvXXu0mjfD9}wJLP!pr zyAL3pb9l~sj$q%szMcEI?(4eV=e?ijJ#YteKv|DE%X-u~7Y%Dp%>mw&0Q=^mA+v3C zU)EI!zXB4pc%(K%F3d*9b_Yt^G|{-ADk>ZV<^%I2NFA!ihl#2&1=}5P`}H_V5cn!E z!O>4v$`C-AUyDU)jG!0Sxa2TQ&ISPrfFgn98kam5%_G4sfGWKLr9fe#gHiWoZN}nT zlu84=%I#N+AlnGA1#ki=)Z2O31`y~B`RPR1fwChC{ce;7!=7HU^RS%(m9PM_Lc~Md zS^&$kZ$AC;yR?4v4yZ=u1Y>B}zJf&P8w15KtbISVa;kPe01s>|AZ=ZqNkWr^emf82 zDR`|*v#kk=MJQ27HF3V%w}-18=TMY;a@Ml>gG13gh7^vd@L6r9{2VpF+kQUJ!Q3?R-0uPKV^syOdizLRu+XjzgtG$mqa%D#_mB)j$P5e%%YXy1%mF%2 zco-iYVRhkdvbVoLFfc~viLYa|fwE4Oy-7)=-7HN9sLBxkjd^T9I2B8$-P1tkrmZYk zdM`;SsboC4g?3Lv>;TYa1<9?+lIf!vb_zaCmur@j%?ih(Dg4#rr`LOuoSiRC*K&3h z)9XFSVE1oPodv#67-Lh_gK{;rKI0Tr-6z+JRJW)5shhe5!_eOJIjHjcnHKtg@d6MW z8v|83JdNa)A29hJ{(FFu`t=ep1T(|!(;}cLw5Agi9LmpRe)3|LWUUF~|LMEPKZ665 zv~(d3N2EznwM#rEA7g+bRmc5)&I8$@^{yY9`R3(k*xl5^^7R|Tdiu_t=F44=v$L_8 zjQpn~B`*yR@UUqIj5lF=7keVjVRHBurLADO;nG>QHkXu)m2{Ygan-_9UNJ4q2vF9e zPLzrnOjVX-t>LM+YCz0y?uosXq_|d_uV!Wc>-~CHWO;^?+ou(Pagyc=fr@IEydPSd zaS8-l6rNYXVL|d#c`?cx(XM5K#x;aWU=Sb(L!wLoR}9?v;P&Y;pfJt=$EsbjEgo*% z|B6*OV<=z2@KiyeEneNY16Yv(CO<5XB_!~uKx+iud)otW