From 58635801fc516206f999d6a1726a1942835ef331 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 21 May 2025 09:55:22 +0200 Subject: [PATCH] add icons for external sources in settings and recent blocks view --- .../sparrow/control/BlockCube.java | 37 ++++++++++- .../sparrow/control/RecentBlocksView.java | 18 +++++- .../sparrow/net/BlockExplorer.java | 23 +++++++ .../sparrow/net/ExchangeSource.java | 16 +++++ .../sparrow/net/FeeRatesSource.java | 24 +++++++ .../settings/GeneralSettingsController.java | 62 +++++++++++++------ .../sparrow/wallet/SendController.java | 5 ++ .../blockexplorer/blockstream.info-icon.svg | 8 +++ .../blockexplorer/mempool.space-icon.svg | 28 +++++++++ .../image/exchangesource/coinbase-icon.svg | 7 +++ .../image/exchangesource/coingecko-icon.svg | 29 +++++++++ .../exchangesource/mempool.space-icon.svg | 28 +++++++++ .../feeratesource/mempool.space-icon.svg | 28 +++++++++ .../image/feeratesource/server-icon.svg | 6 ++ .../image/feeratesource/settings-icon.svg | 6 ++ 15 files changed, 304 insertions(+), 21 deletions(-) create mode 100644 src/main/resources/image/blockexplorer/blockstream.info-icon.svg create mode 100644 src/main/resources/image/blockexplorer/mempool.space-icon.svg create mode 100644 src/main/resources/image/exchangesource/coinbase-icon.svg create mode 100644 src/main/resources/image/exchangesource/coingecko-icon.svg create mode 100644 src/main/resources/image/exchangesource/mempool.space-icon.svg create mode 100644 src/main/resources/image/feeratesource/mempool.space-icon.svg create mode 100644 src/main/resources/image/feeratesource/server-icon.svg create mode 100644 src/main/resources/image/feeratesource/settings-icon.svg diff --git a/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java index 33665bc3..25e40beb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java @@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.sparrow.BlockSummary; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.net.FeeRatesSource; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -15,6 +17,7 @@ import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.util.Duration; +import org.girod.javafx.svgimage.SVGImage; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -32,6 +35,7 @@ public class BlockCube extends Group { private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis()); private final StringProperty elapsedProperty = new SimpleStringProperty(""); private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false); + private final ObjectProperty feeRatesSource = new SimpleObjectProperty<>(null); private Polygon front; private Rectangle unusedArea; @@ -43,10 +47,12 @@ public class BlockCube extends Group { private final TextFlow medianFeeTextFlow = new TextFlow(); private final Text txCountText = new Text(); private final Text elapsedText = new Text(); + private final Group feeRateIcon = new Group(); public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) { getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube"); this.confirmedProperty.set(confirmed); + this.feeRatesSource.set(Config.get().getFeeRatesSource()); this.weightProperty.addListener((_, _, _) -> { if(front != null) { @@ -79,6 +85,11 @@ public class BlockCube extends Group { updateFill(); } }); + this.feeRatesSource.addListener((_, _, _) -> { + if(front != null) { + updateFill(); + } + }); this.medianFeeText.textProperty().addListener((_, _, _) -> { pulse(); }); @@ -145,12 +156,15 @@ public class BlockCube extends Group { txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2); txCountText.setY(34); + feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2); + feeRateIcon.setTranslateY(-36); + elapsedText.getStyleClass().add("block-text"); elapsedText.setFont(new Font(10)); elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2); elapsedText.setY(50); - getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, elapsedText); + getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText); } private void updateFill() { @@ -167,6 +181,7 @@ public class BlockCube extends Group { usedArea.setHeight(CUBE_SIZE - startYAbsolute); usedArea.setVisible(true); heightText.setVisible(true); + feeRateIcon.getChildren().clear(); } else { getStyleClass().removeAll("block-confirmed"); if(!getStyleClass().contains("block-unconfirmed")) { @@ -175,6 +190,14 @@ public class BlockCube extends Group { usedArea.setVisible(false); unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";"); heightText.setVisible(false); + if(feeRatesSource.get() != null) { + SVGImage svgImage = feeRatesSource.get().getSVGImage(); + if(svgImage != null) { + feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage()); + } else { + feeRateIcon.getChildren().clear(); + } + } } } @@ -324,6 +347,18 @@ public class BlockCube extends Group { confirmedProperty.set(confirmed); } + public FeeRatesSource getFeeRatesSource() { + return feeRatesSource.get(); + } + + public ObjectProperty feeRatesSourceProperty() { + return feeRatesSource; + } + + public void setFeeRatesSource(FeeRatesSource feeRatesSource) { + this.feeRatesSource.set(feeRatesSource); + } + public static BlockCube fromBlockSummary(BlockSummary blockSummary) { return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(), blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java index 250025f0..38bb07ef 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java @@ -1,12 +1,15 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.BlockSummary; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.net.FeeRatesSource; import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import javafx.animation.TranslateTransition; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Tooltip; import javafx.scene.layout.Pane; import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; @@ -16,6 +19,8 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; +import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE; + public class RecentBlocksView extends Pane { private static final double CUBE_SPACING = 100; private static final double ANIMATION_DURATION_MILLIS = 1000; @@ -24,6 +29,7 @@ public class RecentBlocksView extends Pane { private final CompositeDisposable disposables = new CompositeDisposable(); private final ObjectProperty> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>()); + private final Tooltip tooltip = new Tooltip(); public RecentBlocksView() { cubesProperty.addListener((_, _, newValue) -> { @@ -41,6 +47,16 @@ public class RecentBlocksView extends Pane { cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp())); } })); + + updateFeeRatesSource(Config.get().getFeeRatesSource()); + Tooltip.install(this, tooltip); + } + + public void updateFeeRatesSource(FeeRatesSource feeRatesSource) { + tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription()); + if(getCubes() != null && !getCubes().isEmpty()) { + getCubes().getFirst().setFeeRatesSource(feeRatesSource); + } } public void drawView() { @@ -54,7 +70,7 @@ public class RecentBlocksView extends Pane { } private void createSeparator() { - Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, 80); + Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, CUBE_SIZE); separator.getStyleClass().add("blocks-separator"); separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern separator.setStrokeWidth(1.0); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java b/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java index bfc37850..ed68907a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java @@ -1,12 +1,22 @@ package com.sparrowwallet.sparrow.net; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Server; +import org.girod.javafx.svgimage.SVGImage; +import org.girod.javafx.svgimage.SVGLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.util.Locale; public enum BlockExplorer { MEMPOOL_SPACE("https://mempool.space"), BLOCKSTREAM_INFO("https://blockstream.info"), NONE("http://none"); + private static final Logger log = LoggerFactory.getLogger(BlockExplorer.class); + private final Server server; BlockExplorer(String url) { @@ -16,4 +26,17 @@ public enum BlockExplorer { public Server getServer() { return server; } + + public static SVGImage getSVGImage(Server server) { + try { + URL url = AppServices.class.getResource("/image/blockexplorer/" + server.getHost().toLowerCase(Locale.ROOT) + "-icon.svg"); + if(url != null) { + return SVGLoader.load(url); + } + } catch(Exception e) { + log.error("Could not load block explorer image for " + server.getHost()); + } + + return null; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java index 29bc2561..2a55b48d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java @@ -9,9 +9,12 @@ import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; import org.apache.commons.lang3.time.DateUtils; +import org.girod.javafx.svgimage.SVGImage; +import org.girod.javafx.svgimage.SVGLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Instant; @@ -297,6 +300,19 @@ public enum ExchangeSource { return name; } + public SVGImage getSVGImage() { + try { + URL url = AppServices.class.getResource("/image/exchangesource/" + name.toLowerCase(Locale.ROOT) + "-icon.svg"); + if(url != null) { + return SVGLoader.load(url); + } + } catch(Exception e) { + log.error("Could not load exchange source image for " + name); + } + + return null; + } + public static class CurrenciesService extends Service> { private final ExchangeSource exchangeSource; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java index 4510f8b5..5b99f782 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -7,9 +7,12 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransactionHash; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.BlockSummary; +import org.girod.javafx.svgimage.SVGImage; +import org.girod.javafx.svgimage.SVGLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URL; import java.util.*; public enum FeeRatesSource { @@ -275,6 +278,27 @@ public enum FeeRatesSource { return name; } + public String getDescription() { + return switch(this) { + case ELECTRUM_SERVER -> "server"; + case MINIMUM -> "settings"; + default -> getName().toLowerCase(Locale.ROOT); + }; + } + + public SVGImage getSVGImage() { + try { + URL url = AppServices.class.getResource("/image/feeratesource/" + getDescription() + "-icon.svg"); + if(url != null) { + return SVGLoader.load(url); + } + } catch(Exception e) { + log.error("Could not load fee rates source image for " + name); + } + + return null; + } + protected record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {} private record OxtRates(OxtRatesData[] data) {} diff --git a/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java b/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java index 9a650501..e0de8135 100644 --- a/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java @@ -87,6 +87,8 @@ public class GeneralSettingsController extends SettingsDetailController { config.setFeeRatesSource(feeRatesSource.getValue()); } + feeRatesSource.setCellFactory(_ -> new FeeRatesSourceListCell()); + feeRatesSource.setButtonCell(feeRatesSource.getCellFactory().call(null)); feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> { config.setFeeRatesSource(newValue); EventManager.get().post(new FeeRatesSourceChangedEvent(newValue)); @@ -96,25 +98,8 @@ public class GeneralSettingsController extends SettingsDetailController { currenciesLoadWarning.setVisible(false); blockExplorers.setItems(getBlockExplorerList()); - blockExplorers.setConverter(new StringConverter<>() { - @Override - public String toString(Server server) { - if(server == null || server == BlockExplorer.NONE.getServer()) { - return "None"; - } - - if(server == CUSTOM_BLOCK_EXPLORER) { - return "Custom..."; - } - - return server.getHost(); - } - - @Override - public Server fromString(String string) { - return null; - } - }); + blockExplorers.setCellFactory(_ -> new BlockExplorerListCell()); + blockExplorers.setButtonCell(blockExplorers.getCellFactory().call(null)); blockExplorers.valueProperty().addListener((observable, oldValue, newValue) -> { if(newValue != null) { if(newValue == CUSTOM_BLOCK_EXPLORER) { @@ -258,14 +243,50 @@ public class GeneralSettingsController extends SettingsDetailController { fiatCurrency.valueProperty().addListener(fiatCurrencyListener); } + private static class FeeRatesSourceListCell extends ListCell { + @Override + protected void updateItem(FeeRatesSource item, boolean empty) { + super.updateItem(item, empty); + if(empty || item == null) { + setText(null); + setGraphic(null); + } else { + setText(item.toString()); + setGraphic(item.getSVGImage()); + setGraphicTextGap(8.0d); + } + } + } + + private static class BlockExplorerListCell extends ListCell { + @Override + protected void updateItem(Server server, boolean empty) { + super.updateItem(server, empty); + if(empty || server == null || server == BlockExplorer.NONE.getServer()) { + setText("None"); + setGraphic(null); + } else if(server == CUSTOM_BLOCK_EXPLORER) { + setText("Custom..."); + setGraphic(null); + } else { + setText(server.getHost()); + setGraphic(BlockExplorer.getSVGImage(server)); + setGraphicTextGap(8.0d); + } + } + } + private static class ExchangeSourceButtonCell extends ListCell { @Override protected void updateItem(ExchangeSource exchangeSource, boolean empty) { super.updateItem(exchangeSource, empty); if(exchangeSource == null || empty) { setText(""); + setGraphic(null); } else { setText(exchangeSource.getName()); + setGraphic(exchangeSource.getSVGImage()); + setGraphicTextGap(8.0d); } } } @@ -276,12 +297,15 @@ public class GeneralSettingsController extends SettingsDetailController { super.updateItem(exchangeSource, empty); if(exchangeSource == null || empty) { setText(""); + setGraphic(null); } else { String text = exchangeSource.getName(); if(exchangeSource.getDescription() != null && !exchangeSource.getDescription().isEmpty()) { text += " (" + exchangeSource.getDescription() + ")"; } setText(text); + setGraphic(exchangeSource.getSVGImage()); + setGraphicTextGap(8.0d); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index f6e69f45..7f1de26a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -1600,6 +1600,11 @@ public class SendController extends WalletFormController implements Initializabl } } + @Subscribe + public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) { + recentBlocksView.updateFeeRatesSource(event.getFeeRateSource()); + } + private class PrivacyAnalysisTooltip extends VBox { private final List