diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index aba5e31..4630487 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -10,8 +10,13 @@ import com.andrewlalis.perfin.view.StartupSplashScreen; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; +import javafx.scene.text.Font; import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -21,6 +26,7 @@ import java.util.function.Consumer; * The class from which the JavaFX-based application starts. */ public class PerfinApp extends Application { + private static final Logger log = LoggerFactory.getLogger(PerfinApp.class); public static final Path APP_DIR = Path.of(System.getProperty("user.home", "."), ".perfin"); public static PerfinApp instance; @@ -36,6 +42,7 @@ public class PerfinApp extends Application { @Override public void start(Stage stage) { instance = this; + loadFonts(); var splashScreen = new StartupSplashScreen(List.of( PerfinApp::defineRoutes, PerfinApp::initAppDir, @@ -97,4 +104,27 @@ public class PerfinApp extends Application { throw e; } } + + private static void loadFonts() { + List fontResources = List.of( + "/font/JetBrainsMono-2.304/fonts/ttf/JetBrainsMono-Medium.ttf", + "/font/Roboto/Roboto-Regular.ttf", + "/font/Roboto/Roboto-Bold.ttf", + "/font/Roboto/Roboto-Italic.ttf", + "/font/Roboto/Roboto-BoldItalic.ttf" + ); + for (String res : fontResources) { + URL resourceUrl = PerfinApp.class.getResource(res); + if (resourceUrl == null) { + log.warn("Font resource {} was not found.", res); + } else { + Font font = Font.loadFont(PerfinApp.class.getResource(res).toExternalForm(), 10); + if (font == null) { + log.warn("Failed to load font {}.", res); + } else { + log.debug("Loaded font: Family = {}, Name = {}, Style = {}.", font.getFamily(), font.getName(), font.getStyle()); + } + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java index 13f8bae..3347df1 100644 --- a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java @@ -25,7 +25,7 @@ public class MainViewController { breadCrumb -> { Label label = new Label("> " + breadCrumb.route()); if (breadCrumb.current()) { - label.setStyle("-fx-font-weight: bold"); + label.getStyleClass().add("bold-text"); } return label; } diff --git a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java index ef0f723..1fb72eb 100644 --- a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java @@ -66,22 +66,15 @@ public class ProfilesViewController { for (String profileName : profileNames) { boolean isCurrent = profileName.equals(currentProfile); AnchorPane profilePane = new AnchorPane(); - profilePane.setStyle(""" - -fx-border-color: lightgray; - -fx-border-radius: 5px; - -fx-padding: 5px; - """); + profilePane.getStyleClass().add("tile"); Text nameTextElement = new Text(profileName); - nameTextElement.setStyle("-fx-font-size: large;"); + nameTextElement.getStyleClass().add("large-font"); TextFlow nameLabel = new TextFlow(nameTextElement); if (isCurrent) { - nameTextElement.setStyle("-fx-font-size: large; -fx-font-weight: bold;"); + nameTextElement.getStyleClass().addAll("large-font", "bold-text"); Text currentProfileIndicator = new Text(" Currently Selected Profile"); - currentProfileIndicator.setStyle(""" - -fx-font-size: small; - -fx-fill: grey; - """); + currentProfileIndicator.getStyleClass().addAll("small-font", "secondary-color-fill"); nameLabel.getChildren().add(currentProfileIndicator); } AnchorPane.setLeftAnchor(nameLabel, 0.0); diff --git a/src/main/java/com/andrewlalis/perfin/model/Account.java b/src/main/java/com/andrewlalis/perfin/model/Account.java index 83146a4..dfb3db7 100644 --- a/src/main/java/com/andrewlalis/perfin/model/Account.java +++ b/src/main/java/com/andrewlalis/perfin/model/Account.java @@ -39,6 +39,16 @@ public class Account extends IdEntity { return "..." + accountNumber.substring(accountNumber.length() - suffixLength); } + public String getAccountNumberGrouped(int groupSize, char separator) { + StringBuilder sb = new StringBuilder(); + int idx = 0; + while (idx < accountNumber.length()) { + sb.append(accountNumber.charAt(idx++)); + if (idx % groupSize == 0 && idx < accountNumber.length()) sb.append(separator); + } + return sb.toString(); + } + public String getShortName() { String numberSuffix = getAccountNumberSuffix(); return name + " (" + numberSuffix + ")"; diff --git a/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java b/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java index 80b0ac2..47a9ab8 100644 --- a/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java +++ b/src/main/java/com/andrewlalis/perfin/model/AccountEntry.java @@ -87,9 +87,10 @@ public class AccountEntry extends IdEntity { * @return The effective value of this entry, either positive or negative. */ public BigDecimal getEffectiveValue(AccountType accountType) { - return switch (accountType) { - case CHECKING, SAVINGS -> type == Type.DEBIT ? amount : amount.negate(); - case CREDIT_CARD -> type == Type.DEBIT ? amount.negate() : amount; - }; + if (accountType.areDebitsPositive()) { + return type == Type.DEBIT ? amount : amount.negate(); + } else { + return type == Type.DEBIT ? amount.negate() : amount; + } } } diff --git a/src/main/java/com/andrewlalis/perfin/model/AccountType.java b/src/main/java/com/andrewlalis/perfin/model/AccountType.java index 79626d9..ef4a127 100644 --- a/src/main/java/com/andrewlalis/perfin/model/AccountType.java +++ b/src/main/java/com/andrewlalis/perfin/model/AccountType.java @@ -4,14 +4,20 @@ package com.andrewlalis.perfin.model; * Represents the different possible account types in Perfin. */ public enum AccountType { - CHECKING("Checking"), - SAVINGS("Savings"), - CREDIT_CARD("Credit Card"); + CHECKING("Checking", true), + SAVINGS("Savings", true), + CREDIT_CARD("Credit Card", false); private final String name; + private final boolean debitsPositive; - AccountType(String name) { + AccountType(String name, boolean debitsPositive) { this.name = name; + this.debitsPositive = debitsPositive; + } + + public boolean areDebitsPositive() { + return debitsPositive; } @Override diff --git a/src/main/java/com/andrewlalis/perfin/view/AccountComboBoxCellFactory.java b/src/main/java/com/andrewlalis/perfin/view/AccountComboBoxCellFactory.java index f6111be..153782d 100644 --- a/src/main/java/com/andrewlalis/perfin/view/AccountComboBoxCellFactory.java +++ b/src/main/java/com/andrewlalis/perfin/view/AccountComboBoxCellFactory.java @@ -23,7 +23,7 @@ public class AccountComboBoxCellFactory implements Callback, L public AccountListCell(String emptyCellText) { this.emptyCellText = emptyCellText; - label.setStyle("-fx-text-fill: black;"); + label.getStyleClass().add("normal-color-text-fill"); } @Override diff --git a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java index 38c94cb..55c63a7 100644 --- a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java +++ b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java @@ -53,7 +53,10 @@ public class StartupSplashScreen extends Stage implements Consumer { textArea.setFocusTraversable(false); Scene scene = new Scene(root, 400.0, 200.0); - scene.getStylesheets().add(StartupSplashScreen.class.getResource("/style/startup-splash-screen.css").toExternalForm()); + scene.getStylesheets().addAll( + StartupSplashScreen.class.getResource("/style/base.css").toExternalForm(), + StartupSplashScreen.class.getResource("/style/startup-splash-screen.css").toExternalForm() + ); return scene; } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java index 81d1359..9704d04 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java @@ -12,14 +12,10 @@ import javafx.scene.layout.BorderPane; */ public abstract class AccountHistoryItemTile extends BorderPane { public AccountHistoryItemTile(AccountHistoryItem item) { - setStyle(""" - -fx-border-color: lightgray; - -fx-border-radius: 5px; - -fx-padding: 5px; - """); + getStyleClass().add("tile"); Label timestampLabel = new Label(DateUtil.formatUTCAsLocalWithZone(item.getTimestamp())); - timestampLabel.setStyle("-fx-font-size: small;"); + timestampLabel.getStyleClass().add("small-font"); setTop(timestampLabel); } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java index 3f5c7dc..90103cf 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java @@ -1,8 +1,11 @@ package com.andrewlalis.perfin.view.component; +import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.AccountType; +import com.andrewlalis.perfin.model.MoneyValue; import com.andrewlalis.perfin.model.Profile; +import javafx.application.Platform; import javafx.geometry.HPos; import javafx.scene.Node; import javafx.scene.control.Label; @@ -14,6 +17,7 @@ import javafx.scene.layout.Priority; import javafx.scene.paint.Color; import javafx.scene.text.Text; +import java.math.BigDecimal; import java.util.Map; import static com.andrewlalis.perfin.PerfinApp.router; @@ -30,14 +34,7 @@ public class AccountTile extends BorderPane { public AccountTile(Account account) { setPrefWidth(350.0); - setStyle(""" - -fx-border-color: lightgray; - -fx-border-width: 1px; - -fx-border-style: solid; - -fx-border-radius: 5px; - -fx-padding: 5px; - -fx-cursor: hand; - """); + getStyleClass().addAll("tile", "hand-cursor"); setTop(getHeader(account)); setBottom(getFooter(account)); @@ -48,7 +45,7 @@ public class AccountTile extends BorderPane { private Node getHeader(Account account) { Text title = new Text("Account #" + account.id); - title.setStyle("-fx-font-size: large; -fx-font-weight: bold;"); + title.getStyleClass().addAll("large-font", "bold-text"); return title; } @@ -56,7 +53,7 @@ public class AccountTile extends BorderPane { Label currencyLabel = new Label(account.getCurrency().getCurrencyCode()); Label typeLabel = new Label(account.getType().toString() + " Account"); HBox footerHBox = new HBox(currencyLabel, typeLabel); - footerHBox.setStyle("-fx-font-size: x-small; -fx-spacing: 3px;"); + footerHBox.getStyleClass().addAll("std-spacing", "small-font"); return footerHBox; } @@ -73,15 +70,33 @@ public class AccountTile extends BorderPane { valueConstraints.setHalignment(HPos.RIGHT); propertiesPane.getColumnConstraints().setAll(keyConstraints, valueConstraints); - Label accountNameLabel = newPropertyValue(account.getName()); + Label accountNameLabel = new Label(account.getName()); accountNameLabel.setWrapText(true); + accountNameLabel.getStyleClass().add("italic-text"); - Label accountTypeLabel = newPropertyValue(account.getType().toString()); + Label accountNumberLabel = new Label(account.getAccountNumber()); + accountNumberLabel.getStyleClass().add("mono-font"); + + Label accountTypeLabel = new Label(account.getType().toString()); accountTypeLabel.setTextFill(ACCOUNT_TYPE_COLORS.get(account.getType())); - accountTypeLabel.setStyle("-fx-font-weight: bold;"); + accountTypeLabel.getStyleClass().add("bold-text"); - Label balanceLabel = newPropertyValue("Computing balance..."); + Label balanceLabel = new Label("Computing balance..."); + balanceLabel.getStyleClass().addAll("mono-font"); balanceLabel.setDisable(true); + Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> { + BigDecimal balance = repo.deriveCurrentBalance(account.id); + String text = CurrencyUtil.formatMoney(new MoneyValue(balance, account.getCurrency())); + Platform.runLater(() -> { + balanceLabel.setText(text); + if (account.getType().areDebitsPositive() && balance.compareTo(BigDecimal.ZERO) < 0) { + balanceLabel.getStyleClass().add("negative-color-text-fill"); + } else if (!account.getType().areDebitsPositive() && balance.compareTo(BigDecimal.ZERO) < 0) { + balanceLabel.getStyleClass().add("positive-color-text-fill"); + } + balanceLabel.setDisable(false); + }); + })); Profile.getCurrent().getDataSource().getAccountBalanceText(account, text -> { balanceLabel.setText(text); balanceLabel.setDisable(false); @@ -91,7 +106,7 @@ public class AccountTile extends BorderPane { newPropertyLabel("Account Name"), accountNameLabel, newPropertyLabel("Account Number"), - newPropertyValue(account.getAccountNumber()), + accountNumberLabel, newPropertyLabel("Account Type"), accountTypeLabel, newPropertyLabel("Current Balance"), @@ -102,18 +117,7 @@ public class AccountTile extends BorderPane { private static Label newPropertyLabel(String text) { Label lbl = new Label(text); - lbl.setStyle(""" - -fx-font-weight: bold; - """); - return lbl; - } - - private static Label newPropertyValue(String text) { - Label lbl = new Label(text); - lbl.setStyle(""" - -fx-font-family: monospace; - -fx-font-size: large; - """); + lbl.getStyleClass().add("bold-text"); return lbl; } } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java index 92f05ed..7e15772 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java @@ -27,7 +27,7 @@ public class AttachmentPreview extends BorderPane { public AttachmentPreview(Attachment attachment) { BorderPane contentContainer = new BorderPane(); Label nameLabel = new Label(attachment.getFilename()); - nameLabel.setStyle("-fx-font-size: small;"); + nameLabel.getStyleClass().add("small-font"); VBox nameContainer = new VBox(nameLabel); nameContainer.setPrefHeight(LABEL_SIZE); nameContainer.setMaxHeight(LABEL_SIZE); diff --git a/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java index baf6fcb..2753ebf 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java @@ -27,42 +27,30 @@ import static com.andrewlalis.perfin.PerfinApp.router; */ public class TransactionTile extends BorderPane { public final BooleanProperty selected = new SimpleBooleanProperty(false); - private static final String UNSELECTED_STYLE = """ - -fx-border-color: lightgray; - -fx-border-width: 1px; - -fx-border-style: solid; - -fx-border-radius: 5px; - -fx-padding: 5px; - -fx-cursor: hand; - """; - private static final String SELECTED_STYLE = """ - -fx-border-color: white; - -fx-border-width: 1px; - -fx-border-style: solid; - -fx-border-radius: 5px; - -fx-padding: 5px; - -fx-cursor: hand; - """; public TransactionTile(Transaction transaction) { - setStyle(UNSELECTED_STYLE); + getStyleClass().addAll("tile", "hand-cursor"); setTop(getHeader(transaction)); setCenter(getBody(transaction)); setBottom(getFooter(transaction)); - styleProperty().bind(selected.map(value -> value ? SELECTED_STYLE : UNSELECTED_STYLE)); + selected.addListener((observable, oldValue, newValue) -> { + if (newValue) { + getStyleClass().add("tile-border-selected"); + } else { + getStyleClass().remove("tile-border-selected"); + } + }); } private Node getHeader(Transaction transaction) { Label currencyLabel = new Label(CurrencyUtil.formatMoney(transaction.getMoneyAmount())); - currencyLabel.setStyle("-fx-font-family: monospace;"); + currencyLabel.getStyleClass().add("mono-font"); HBox headerHBox = new HBox( currencyLabel ); - headerHBox.setStyle(""" - -fx-spacing: 3px; - """); + headerHBox.getStyleClass().addAll("std-spacing"); return headerHBox; } @@ -77,14 +65,14 @@ public class TransactionTile extends BorderPane { Hyperlink link = new Hyperlink(acc.getShortName()); link.setOnAction(event -> router.navigate("account", acc)); Text prefix = new Text("Credited from"); - prefix.setFill(Color.RED); + prefix.getStyleClass().add("negative-color-fill"); Platform.runLater(() -> bodyVBox.getChildren().add(new TextFlow(prefix, link))); }); accounts.ifDebit(acc -> { Hyperlink link = new Hyperlink(acc.getShortName()); link.setOnAction(event -> router.navigate("account", acc)); Text prefix = new Text("Debited to"); - prefix.setFill(Color.GREEN); + prefix.getStyleClass().add("positive-color-fill"); Platform.runLater(() -> bodyVBox.getChildren().add(new TextFlow(prefix, link))); }); }); @@ -96,10 +84,7 @@ public class TransactionTile extends BorderPane { HBox footerHBox = new HBox( timestampLabel ); - footerHBox.setStyle(""" - -fx-spacing: 3px; - -fx-font-size: small; - """); + footerHBox.getStyleClass().addAll("std-spacing", "small-font"); return footerHBox; } diff --git a/src/main/resources/account-view.fxml b/src/main/resources/account-view.fxml index 3e188bf..a2e411f 100644 --- a/src/main/resources/account-view.fxml +++ b/src/main/resources/account-view.fxml @@ -37,7 +37,7 @@ @@ -65,7 +65,7 @@