diff --git a/wallet-ui/build.gradle b/wallet-ui/build.gradle index ec3d645..ae4765d 100644 --- a/wallet-ui/build.gradle +++ b/wallet-ui/build.gradle @@ -27,6 +27,53 @@ test { useJUnitPlatform() } +// The version shown in Settings > About comes from gradle.properties at BUILD +// time. A hand-edited constant is wrong the moment someone forgets to bump it, +// and the version is the first thing a bug report quotes. +// +// Generated as Java SOURCE, not a properties resource: native-image includes +// only resources that are explicitly registered, so a resource would work in the +// jar build and silently come back null in the native one — the version would be +// missing exactly where it is hardest to notice. +def buildInfoDir = layout.buildDirectory.dir('generated/sources/buildinfo') + +def generateBuildInfo = tasks.register('generateBuildInfo') { + description = 'Generate BuildInfo.java from gradle.properties' + def walletVersion = project.version.toString() + def nodeVersion = (rootProject.findProperty('yanoVersion') ?: 'unknown').toString() + def outDir = buildInfoDir + inputs.property('walletVersion', walletVersion) + inputs.property('nodeVersion', nodeVersion) + outputs.dir(outDir) + doLast { + def pkg = new File(outDir.get().asFile, 'com/bloxbean/cardano/yano/wallet/ui') + pkg.mkdirs() + new File(pkg, 'BuildInfo.java').text = """package com.bloxbean.cardano.yano.wallet.ui; + +/** + * Build-time facts, generated by :wallet-ui:generateBuildInfo from + * gradle.properties. Do not edit — your changes are overwritten on every build. + */ +public final class BuildInfo { + + /** The wallet's version, e.g. "0.1.0-pre4". */ + public static final String WALLET_VERSION = "${walletVersion}"; + + /** The managed Yano node this build is pinned to. */ + public static final String NODE_VERSION = "${nodeVersion}"; + + private BuildInfo() { + } +} +""" + } +} + +// Registering the provider (not the directory) makes compileJava depend on the +// generating task, so a plain `./gradlew build` cannot compile a stale copy. +sourceSets.main.java.srcDir generateBuildInfo + + def detectJavafxPlatform() { def osName = System.getProperty('os.name').toLowerCase() def osArch = System.getProperty('os.arch').toLowerCase() diff --git a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/Shell.java b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/Shell.java index 6725315..b44390f 100644 --- a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/Shell.java +++ b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/Shell.java @@ -5,14 +5,13 @@ import com.bloxbean.cardano.yano.wallet.ui.screens.HistoryScreen; import com.bloxbean.cardano.yano.wallet.ui.screens.ReceiveScreen; import com.bloxbean.cardano.yano.wallet.ui.screens.SendScreen; +import com.bloxbean.cardano.yano.wallet.ui.screens.AboutScreen; import com.bloxbean.cardano.yano.wallet.ui.screens.SettingsScreen; import com.bloxbean.cardano.yano.wallet.ui.screens.GovernanceScreen; import com.bloxbean.cardano.yano.wallet.ui.screens.ProposalsScreen; import com.bloxbean.cardano.yano.wallet.ui.screens.LiveScreen; import com.bloxbean.cardano.yano.wallet.ui.screens.StakingScreen; -import com.bloxbean.cardano.yano.wallet.ui.live.LiveBlocksView; import com.bloxbean.cardano.yano.wallet.ui.live.LiveChainModel; -import com.bloxbean.cardano.yano.wallet.ui.live.LivePrefs; import com.bloxbean.cardano.yano.wallet.ui.util.Icons; import com.bloxbean.cardano.yano.wallet.ui.util.Ui; import javafx.animation.KeyFrame; @@ -64,7 +63,6 @@ public class Shell { private final Tooltip syncTooltip = new Tooltip(); private final Timeline statusPoller; private final LiveChainModel liveModel = new LiveChainModel(); - private LiveBlocksView ambient; private String active; // Delta tracking to tell "advancing" from "stalled" during catch-up without // any network-tip math: remember the highest block seen and when it last rose. @@ -110,6 +108,9 @@ public Shell(WalletUiController controller, Runnable onLock, registry.put("Proposals", () -> new ProposalsScreen(controller, overlay)); registry.put("Live", () -> new LiveScreen(controller, overlay, liveModel)); registry.put("Settings", () -> new SettingsScreen(controller, overlay, this::changeNetwork)); + // Takes no controller: build facts come from BuildInfo, generated at build + // time, so this screen works even when nothing else does. + registry.put("About", () -> new AboutScreen(overlay)); statusPoller = new Timeline(new KeyFrame(Duration.seconds(5), e -> tick())); statusPoller.setCycleCount(Timeline.INDEFINITE); @@ -119,14 +120,8 @@ public StackPane root() { BorderPane frame = new BorderPane(); frame.getStyleClass().add("shell"); frame.setLeft(buildSidebar()); - // Ambient live-chain background sits behind every screen; screens have - // transparent gaps, so it shows through without touching their styling. - ambient = new LiveBlocksView(liveModel, true); - StackPane center = new StackPane(ambient, content); - frame.setCenter(center); + frame.setCenter(content); overlay.getChildren().add(frame); - syncAmbient(); - LivePrefs.setOnChange(() -> javafx.application.Platform.runLater(this::syncAmbient)); navigate("Dashboard"); statusPoller.play(); pollStatus(); @@ -135,27 +130,11 @@ public StackPane root() { public void dispose() { statusPoller.stop(); - LivePrefs.setOnChange(null); - if (ambient != null) { - ambient.dispose(); - } - } - - /** Reflect the ambient-background toggle: show/hide + start/stop the animation. */ - private void syncAmbient() { - boolean on = LivePrefs.ambientEnabled(); - if (ambient != null) { - ambient.setVisible(on); - ambient.setManaged(on); - ambient.setActive(on); - } - if (on) { - pollLiveChain(); - } } + /** The Live page is the only thing that consumes chain-tip data now. */ private boolean liveNeeded() { - return LivePrefs.ambientEnabled() || "Live".equals(active); + return "Live".equals(active); } private void pollLiveChain() { @@ -216,6 +195,7 @@ private Node buildSidebar() { addNav(side, "Proposals", Icons.PROPOSALS); addNav(side, "Live", Icons.LIVE); addNav(side, "Settings", Icons.SETTINGS); + addNav(side, "About", Icons.ABOUT); Region grow = new Region(); VBox.setVgrow(grow, Priority.ALWAYS); diff --git a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/contract/WalletUiController.java b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/contract/WalletUiController.java index 2f6c2ee..64b18d8 100644 --- a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/contract/WalletUiController.java +++ b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/contract/WalletUiController.java @@ -99,7 +99,7 @@ public interface WalletUiController { String dataDir(); /** - * Live chain-tip snapshot for the animated "Live" view / ambient background. + * Live chain-tip snapshot for the animated "Live" view. * Best-effort: returns {@link LiveChainView#unreachable()} rather than throwing * when the node is unavailable, so the visualization degrades quietly. */ diff --git a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/live/LiveBlocksView.java b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/live/LiveBlocksView.java index 4eeaba6..8e0c3a7 100644 --- a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/live/LiveBlocksView.java +++ b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/live/LiveBlocksView.java @@ -22,13 +22,12 @@ /** * A Canvas that animates the chain tip as a train of block tiles sliding in from * the right — newest at the leading edge, older easing left, each tile brightening - * with its transaction count. Reused in two modes: {@code ambient} (dim, behind - * the shell content) and full (the dedicated Live page). + * with its transaction count. * - *

Runs an {@link AnimationTimer} only while it is {@link #setActive active}, - * attached to a scene, AND its window is focused — so it costs nothing in the - * background or when the feature is off. All listeners are removed on detach / - * {@link #dispose}, so it does not leak across shell rebuilds. + *

The {@code ambient} mode (dim, mouse-transparent, drawn behind every + * screen) is no longer reachable: the wallet dropped the animated background + * setting it existed for. The flag is kept because unpicking it means editing + * the drawing code the Live page depends on, for no user-visible gain. */ public final class LiveBlocksView extends Region { private static final double TILE_W = 104; // full-mode block card (px) @@ -76,7 +75,7 @@ public void handle(long now) { sceneProperty().addListener((o, oldScene, newScene) -> onSceneChanged(newScene)); } - /** External gate: the ambient toggle, or the Live page being on screen. */ + /** External gate: whether the Live page is on screen. */ public void setActive(boolean on) { if (on == active) { return; diff --git a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/live/LivePrefs.java b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/live/LivePrefs.java deleted file mode 100644 index 87ea5dc..0000000 --- a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/live/LivePrefs.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.bloxbean.cardano.yano.wallet.ui.live; - -import java.util.prefs.Preferences; - -/** - * Persisted toggle for the opt-in animated "live chain" ambient background. - * Off by default — the animation costs frames, so a user opts in. Stored per-user - * (like the theme), independent of the wallet data directory. - */ -public final class LivePrefs { - private LivePrefs() { - } - - private static final Preferences PREFS = - Preferences.userRoot().node("com/bloxbean/cardano/yano/wallet/ui"); - private static final String KEY = "liveBackground"; - - private static volatile boolean ambientEnabled = PREFS.getBoolean(KEY, false); - // A single listener (the live shell) so a Settings toggle updates the ambient - // layer immediately. The shell registers on show and clears it on dispose. - private static volatile Runnable onChange; - - public static boolean ambientEnabled() { - return ambientEnabled; - } - - public static void setAmbientEnabled(boolean enabled) { - ambientEnabled = enabled; - PREFS.putBoolean(KEY, enabled); - Runnable listener = onChange; - if (listener != null) { - listener.run(); - } - } - - /** Register (or clear, with {@code null}) the change listener; last one wins. */ - public static void setOnChange(Runnable listener) { - onChange = listener; - } -} diff --git a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/screens/AboutScreen.java b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/screens/AboutScreen.java new file mode 100644 index 0000000..7464c8f --- /dev/null +++ b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/screens/AboutScreen.java @@ -0,0 +1,123 @@ +package com.bloxbean.cardano.yano.wallet.ui.screens; + +import com.bloxbean.cardano.yano.wallet.ui.BuildInfo; +import com.bloxbean.cardano.yano.wallet.ui.Shell; +import com.bloxbean.cardano.yano.wallet.ui.util.Icons; +import com.bloxbean.cardano.yano.wallet.ui.util.Ui; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + +/** + * Which build this is. + * + *

A top-level screen rather than a card inside Settings: the first question + * on any bug report is "which version?", and someone answering it should not + * have to know that versions live under Settings. + * + *

Every value comes from {@link BuildInfo}, generated from gradle.properties + * at build time, so this cannot drift from the release it shipped in. + */ +public class AboutScreen implements Shell.Screen { + private static final String REPO = "https://github.com/bloxbean/yano-wallet"; + + private final StackPane overlay; + private final ScrollPane root; + + public AboutScreen(StackPane overlay) { + this.overlay = overlay; + this.root = build(); + } + + private ScrollPane build() { + Label title = new Label("About"); + title.getStyleClass().add("screen-title"); + + VBox versionCard = Ui.card("Version", + mono("Yano Wallet " + BuildInfo.WALLET_VERSION), + mono("Managed node: Yano " + BuildInfo.NODE_VERSION), + mono("Runtime: " + runtimeDescription()), + mono("Platform: " + platformDescription()), + Ui.muted("Include these details in a bug report — they identify the exact " + + "build, which nothing else in the app can tell you."), + copyButton()); + + Hyperlink repo = new Hyperlink(REPO); + repo.setOnAction(e -> { + Ui.copyToClipboard(REPO); + Ui.toast(overlay, "Link copied", false); + }); + + VBox projectCard = Ui.card("Project", + Ui.muted("A full-node Cardano wallet: it runs its own Yano node, so balances " + + "and history come from a chain you validated yourself."), + repo, + Ui.muted("MIT licensed. Under active development and not audited — use test " + + "networks, and never a recovery phrase holding real funds.")); + + VBox column = new VBox(16, title, versionCard, projectCard); + column.setPadding(new Insets(24)); + column.setMaxWidth(860); + + ScrollPane scroll = new ScrollPane(column); + scroll.setFitToWidth(true); + scroll.getStyleClass().add("screen-scroll"); + return scroll; + } + + private Button copyButton() { + Button copy = new Button("Copy version details", Icons.icon(Icons.COPY, 14, "nav-icon")); + copy.getStyleClass().add("primary-button"); + copy.setOnAction(e -> { + Ui.copyToClipboard(report()); + Ui.toast(overlay, "Version details copied", false); + }); + return copy; + } + + private static Label mono(String text) { + Label label = new Label(text); + label.getStyleClass().add("mono"); + label.setWrapText(true); + return label; + } + + /** + * A native image has no JVM to report, and printing a Java version there + * would send a bug report down the wrong path — several native-image + * failures reproduce only in that build. + */ + private static String runtimeDescription() { + if (System.getProperty("org.graalvm.nativeimage.imagecode") != null) { + return "native image (no JVM)"; + } + return "Java " + Runtime.version().feature() + " (" + System.getProperty("java.vendor") + ")"; + } + + private static String platformDescription() { + return System.getProperty("os.name") + " " + System.getProperty("os.version") + + " (" + System.getProperty("os.arch") + ")"; + } + + private static String report() { + return "Yano Wallet " + BuildInfo.WALLET_VERSION + + "\nManaged node: Yano " + BuildInfo.NODE_VERSION + + "\nRuntime: " + runtimeDescription() + + "\nPlatform: " + platformDescription(); + } + + @Override + public Node root() { + return root; + } + + @Override + public void refresh() { + // Build facts are fixed for the life of the process. + } +} diff --git a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/screens/SettingsScreen.java b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/screens/SettingsScreen.java index f9d292b..85ec821 100644 --- a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/screens/SettingsScreen.java +++ b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/screens/SettingsScreen.java @@ -2,7 +2,6 @@ import com.bloxbean.cardano.yano.wallet.ui.Shell; import com.bloxbean.cardano.yano.wallet.ui.contract.WalletUiController; -import com.bloxbean.cardano.yano.wallet.ui.live.LivePrefs; import com.bloxbean.cardano.yano.wallet.ui.util.ThemeManager; import com.bloxbean.cardano.yano.wallet.ui.util.Ui; import javafx.application.Platform; @@ -294,15 +293,9 @@ private VBox appearanceCard() { } }); }); - CheckBox liveBackground = new CheckBox("Animated live-chain background"); - liveBackground.setSelected(LivePrefs.ambientEnabled()); - liveBackground.setOnAction(e -> LivePrefs.setAmbientEnabled(liveBackground.isSelected())); return Ui.card("Appearance", Ui.muted("Color theme — applies instantly and is remembered."), - themePicker, - liveBackground, - Ui.muted("A subtle animation of live blocks behind the app. Opt-in; it pauses when the " - + "window isn't focused. Open the Live page for the full view.")); + themePicker); } private void loadNodeLog() { diff --git a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/util/Icons.java b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/util/Icons.java index f766b91..f4bca4f 100644 --- a/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/util/Icons.java +++ b/wallet-ui/src/main/java/com/bloxbean/cardano/yano/wallet/ui/util/Icons.java @@ -27,6 +27,9 @@ private Icons() { + " 2-2h12M16 13h3"; public static final String LOCK = "M7 11V7a5 5 0 0 1 10 0v4M5 11h14v10H5V11z"; // Filled equalizer bars — reads as live activity/stats (must be solid, not stroked). + /** "i" in a circle. */ + public static final String ABOUT = "M12 3a9 9 0 1 0 9 9 9 9 0 0 0-9-9zm0 4h.01M11 11h1v6h1"; + public static final String LIVE = "M4 13h3v7H4v-7z M10 8h3v12h-3V8z M16 3h3v17h-3V3z"; public static Region icon(String svgPath, double size, String styleClass) {