mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
add paynym addresses dialog
This commit is contained in:
parent
58f20dab60
commit
a10bdef484
11 changed files with 262 additions and 26 deletions
|
@ -123,15 +123,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
|
||||
if(canSignMessage(nodeEntry.getNode().getWallet())) {
|
||||
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
}
|
||||
|
||||
if(canSignMessage(nodeEntry.getNode())) {
|
||||
Button signMessageButton = new Button("");
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
|
@ -151,7 +154,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(hashIndexEntry.getHashIndex().toString());
|
||||
|
@ -175,7 +178,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
actionBox.getChildren().add(spendUtxoButton);
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
setGraphic(getTreeTableView().getStyleClass().contains("bip47") ? null : actionBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -287,9 +290,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(Wallet wallet) {
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) &&
|
||||
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
|
@ -502,16 +507,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
}
|
||||
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getNode().getWallet())) {
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
package com.sparrowwallet.sparrow.paynym;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.control.AddressTreeTable;
|
||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PayNymAddressesController {
|
||||
|
||||
@FXML
|
||||
private ComboBox<WalletForm> payNymWalletForms;
|
||||
|
||||
@FXML
|
||||
private AddressTreeTable receiveTable;
|
||||
|
||||
@FXML
|
||||
private AddressTreeTable sendTable;
|
||||
|
||||
public void initializeView(WalletForm walletForm) {
|
||||
payNymWalletForms.setItems(FXCollections.observableList(walletForm.getNestedWalletForms().stream().filter(nested -> nested.getWallet().isBip47()).collect(Collectors.toList())));
|
||||
payNymWalletForms.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(WalletForm nested) {
|
||||
return nested == null ? "" : nested.getWallet().getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletForm fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Optional<WalletForm> optInitial = walletForm.getNestedWalletForms().stream().filter(nested -> nested.getWallet().isBip47() && nested.getWallet().getScriptType() == ScriptType.P2WPKH).findFirst();
|
||||
if(optInitial.isPresent()) {
|
||||
optInitial.get().getAccountEntries().clear();
|
||||
receiveTable.initialize(optInitial.get().getNodeEntry(KeyPurpose.RECEIVE));
|
||||
sendTable.initialize(optInitial.get().getNodeEntry(KeyPurpose.SEND));
|
||||
payNymWalletForms.getSelectionModel().select(optInitial.get());
|
||||
}
|
||||
|
||||
payNymWalletForms.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, selected) -> {
|
||||
selected.getAccountEntries().clear();
|
||||
receiveTable.updateAll(selected.getNodeEntry(KeyPurpose.RECEIVE));
|
||||
sendTable.updateAll(selected.getNodeEntry(KeyPurpose.SEND));
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(event.getWallet().equals(payNymWalletForms.getValue().getWallet())) {
|
||||
List<WalletNode> receiveNodes = event.getReceiveNodes();
|
||||
if(!receiveNodes.isEmpty()) {
|
||||
receiveTable.updateHistory(receiveNodes);
|
||||
}
|
||||
|
||||
List<WalletNode> sendNodes = event.getChangeNodes();
|
||||
if(!sendNodes.isEmpty()) {
|
||||
sendTable.updateHistory(sendNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(event.getWallet().equals(payNymWalletForms.getValue().getWallet())) {
|
||||
for(Entry entry : event.getEntries()) {
|
||||
receiveTable.updateLabel(entry);
|
||||
sendTable.updateLabel(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.sparrowwallet.sparrow.paynym;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PayNymAddressesDialog extends Dialog<Boolean> {
|
||||
public PayNymAddressesDialog(WalletForm walletForm) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("paynym/paynymaddresses.fxml"));
|
||||
dialogPane.setContent(payNymLoader.load());
|
||||
PayNymAddressesController controller = payNymLoader.getController();
|
||||
controller.initializeView(walletForm);
|
||||
|
||||
EventManager.get().register(controller);
|
||||
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().add(doneButtonType);
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(controller);
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == doneButtonType ? Boolean.TRUE : Boolean.FALSE);
|
||||
|
||||
dialogPane.setPrefWidth(800);
|
||||
dialogPane.setPrefHeight(600);
|
||||
|
||||
setResizable(true);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
|||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("soroban/paynym.fxml"));
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("paynym/paynym.fxml"));
|
||||
dialogPane.setContent(payNymLoader.load());
|
||||
PayNymController payNymController = payNymLoader.getController();
|
||||
payNymController.initializeView(walletId, selectLinkedOnly);
|
||||
|
@ -30,7 +30,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
|||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/paynym.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("paynym/paynym.css").toExternalForm());
|
||||
|
||||
final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select Contact", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
|
|
@ -1351,7 +1351,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
|
||||
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change
|
||||
String label = outputIndexLabels.containsKey((int)output.getIndex()) ? outputIndexLabels.get((int)output.getIndex()) : headersForm.getName();
|
||||
output.setLabel(label + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
output.setLabel(label + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? (walletNode.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)"));
|
||||
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()));
|
||||
}
|
||||
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
|
||||
|
|
|
@ -9,9 +9,11 @@ import com.sparrowwallet.sparrow.AppServices;
|
|||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.AddressTreeTable;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymAddressesDialog;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -34,6 +36,9 @@ public class AddressesController extends WalletFormController implements Initial
|
|||
@FXML
|
||||
private AddressTreeTable changeTable;
|
||||
|
||||
@FXML
|
||||
private Button showPayNymAddresses;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
|
@ -43,6 +48,9 @@ public class AddressesController extends WalletFormController implements Initial
|
|||
public void initializeView() {
|
||||
receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE));
|
||||
changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
|
||||
|
||||
showPayNymAddresses.managedProperty().bind(showPayNymAddresses.visibleProperty());
|
||||
showPayNymAddresses.setVisible(getWalletForm().getWallet().getChildWallets().stream().anyMatch(Wallet::isBip47));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
@ -110,6 +118,13 @@ public class AddressesController extends WalletFormController implements Initial
|
|||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
showPayNymAddresses.setVisible(getWalletForm().getWallet().getChildWallets().stream().anyMatch(Wallet::isBip47));
|
||||
}
|
||||
}
|
||||
|
||||
public void exportReceiveAddresses(ActionEvent event) {
|
||||
exportAddresses(KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
@ -151,4 +166,9 @@ public class AddressesController extends WalletFormController implements Initial
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNymAddresses(ActionEvent event) {
|
||||
PayNymAddressesDialog payNymAddressesDialog = new PayNymAddressesDialog(getWalletForm());
|
||||
payNymAddressesDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -377,6 +377,10 @@ public class WalletForm {
|
|||
this.lockedProperty.set(locked);
|
||||
}
|
||||
|
||||
public List<NodeEntry> getAccountEntries() {
|
||||
return accountEntries;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletDataChanged(WalletDataChangedEvent event) {
|
||||
if(event.getWallet().equals(wallet)) {
|
||||
|
@ -471,7 +475,7 @@ public class WalletForm {
|
|||
}
|
||||
|
||||
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) {
|
||||
receivedRef.setLabel(changedNode.getLabel() + (changedNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
receivedRef.setLabel(changedNode.getLabel() + (changedNode.getKeyPurpose() == KeyPurpose.CHANGE ? (changedNode.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)"));
|
||||
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, changedNode.getKeyPurpose()));
|
||||
}
|
||||
}
|
||||
|
@ -496,7 +500,7 @@ public class WalletForm {
|
|||
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
|
||||
if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) {
|
||||
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) {
|
||||
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? (event.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)"));
|
||||
labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry);
|
||||
}
|
||||
if((childNode.getLabel() == null || childNode.getLabel().isEmpty())) {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import com.sparrowwallet.sparrow.control.AddressTreeTable?>
|
||||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
|
||||
<?import tornadofx.control.Fieldset?>
|
||||
<?import tornadofx.control.Form?>
|
||||
<?import tornadofx.control.Field?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<StackPane stylesheets="@../wallet/addresses.css, @../wallet/wallet.css, @paynym.css, @../general.css" styleClass="paynym-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.paynym.PayNymAddressesController">
|
||||
<VBox>
|
||||
<HBox styleClass="title-area">
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label text="PayNym Addresses" styleClass="title-label" />
|
||||
</HBox>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<ImageView AnchorPane.rightAnchor="0">
|
||||
<Image url="/image/paynym.png" requestedWidth="50" requestedHeight="50" smooth="false" />
|
||||
</ImageView>
|
||||
</HBox>
|
||||
<BorderPane>
|
||||
<padding>
|
||||
<Insets left="25" right="25" bottom="25" />
|
||||
</padding>
|
||||
<center>
|
||||
<VBox spacing="15">
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="" styleClass="header">
|
||||
<Field text="PayNym:">
|
||||
<ComboBox fx:id="payNymWalletForms" />
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="1">
|
||||
<top>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="addresses-treetable-label" text="Receive Addresses"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<AddressTreeTable fx:id="receiveTable" maxHeight="160" styleClass="bip47" />
|
||||
</center>
|
||||
</BorderPane>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="2">
|
||||
<top>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="addresses-treetable-label" text="Send Addresses"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<AddressTreeTable fx:id="sendTable" maxHeight="160" styleClass="bip47" />
|
||||
</center>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
</center>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
</StackPane>
|
|
@ -33,6 +33,15 @@
|
|||
<Tooltip text="Export receive addresses as CSV" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
<Region HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="showPayNymAddresses" onAction="#showPayNymAddresses" text="PayNyms">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ROBOT" fontSize="12" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Show PayNym addresses" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
|
|
Loading…
Reference in a new issue