add icons for external sources in settings and recent blocks view

This commit is contained in:
Craig Raw
2025-05-21 09:55:22 +02:00
parent 8c32bb3903
commit 58635801fc
15 changed files with 304 additions and 21 deletions
@@ -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> 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<FeeRatesSource> 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);
@@ -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<List<BlockCube>> 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);
@@ -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;
}
}
@@ -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<List<Currency>> {
private final ExchangeSource exchangeSource;
@@ -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) {}
@@ -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<FeeRatesSource> {
@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<Server> {
@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<ExchangeSource> {
@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);
}
}
}
@@ -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<Label> analysisLabels = new ArrayList<>();