diff --git a/design/perfin-logo.svg b/design/perfin-logo.svg new file mode 100644 index 0000000..2dcc35f --- /dev/null +++ b/design/perfin-logo.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index 7d72d04..9b578e2 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -3,6 +3,7 @@ package com.andrewlalis.perfin; import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView; import com.andrewlalis.javafx_scene_router.SceneRouter; import com.andrewlalis.perfin.model.Profile; +import com.andrewlalis.perfin.view.ImageCache; import com.andrewlalis.perfin.view.SceneUtil; import com.andrewlalis.perfin.view.StartupSplashScreen; import javafx.application.Application; @@ -20,6 +21,7 @@ import java.util.function.Consumer; */ public class PerfinApp extends Application { public static final Path APP_DIR = Path.of(System.getProperty("user.home", "."), ".perfin"); + public static PerfinApp instance; /** * The router that's used for navigating between different "pages" in the application. @@ -32,6 +34,7 @@ public class PerfinApp extends Application { @Override public void start(Stage stage) { + instance = this; var splashScreen = new StartupSplashScreen(List.of( PerfinApp::defineRoutes, PerfinApp::initAppDir, @@ -51,6 +54,7 @@ public class PerfinApp extends Application { Scene mainViewScene = SceneUtil.load("/main-view.fxml"); stage.setScene(mainViewScene); stage.setTitle("Perfin"); + stage.getIcons().add(ImageCache.getLogo64()); }); } diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java index f66818a..f3e51b0 100644 --- a/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/AccountViewController.java @@ -1,20 +1,16 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; -import com.andrewlalis.perfin.data.AccountHistoryItemRepository; -import com.andrewlalis.perfin.data.util.CurrencyUtil; import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.history.AccountHistoryItem; +import com.andrewlalis.perfin.view.component.AccountHistoryItemTile; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.*; -import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; import java.time.LocalDateTime; import java.util.List; @@ -39,7 +35,7 @@ public class AccountViewController implements RouteSelectionListener { @Override public void onRouteSelected(Object context) { account = (Account) context; - titleLabel.setText("Account: " + account.getAccountNumber()); + titleLabel.setText("Account #" + account.getId()); accountNameField.setText(account.getName()); accountNumberField.setText(account.getAccountNumber()); @@ -105,42 +101,13 @@ public class AccountViewController implements RouteSelectionListener { } else { loadHistoryFrom = historyItems.getLast().getTimestamp(); } - List nodes = historyItems.stream().map(item -> visualizeHistoryItem(item, historyRepo)).toList(); + List nodes = historyItems.stream() + .map(item -> new AccountHistoryItemTile(item, historyRepo)) + .toList(); Platform.runLater(() -> historyItemsVBox.getChildren().addAll(nodes)); } catch (Exception e) { throw new RuntimeException(e); } }); } - - private Node visualizeHistoryItem(AccountHistoryItem item, AccountHistoryItemRepository repo) { - BorderPane containerPane = new BorderPane(); - containerPane.setStyle(""" - -fx-border-color: lightgray; - -fx-border-radius: 5px; - -fx-padding: 5px; - """); - Label timestampLabel = new Label(item.getTimestamp().format(DateUtil.DEFAULT_DATETIME_FORMAT)); - timestampLabel.setStyle("-fx-font-size: small;"); - containerPane.setTop(timestampLabel); - containerPane.setCenter(switch (item.getType()) { - case TEXT -> { - var text = repo.getTextItem(item.getId()); - yield new TextFlow(new Text(text)); - } - case ACCOUNT_ENTRY -> { - var entry = repo.getAccountEntryItem(item.getId()); - Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency())); - TextFlow text = new TextFlow(new Text("Entry added with value of "), amountText); - yield text; - } - case BALANCE_RECORD -> { - var balanceRecord = repo.getBalanceRecordItem(item.getId()); - Text amountText = new Text(CurrencyUtil.formatMoney(balanceRecord.getBalance(), balanceRecord.getCurrency())); - TextFlow text = new TextFlow(new Text("Balance record added with value of "), amountText); - yield text; - } - }); - return containerPane; - } } diff --git a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java index b79dd12..21507d9 100644 --- a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java @@ -1,5 +1,6 @@ package com.andrewlalis.perfin.control; +import com.andrewlalis.perfin.PerfinApp; import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.view.ProfilesStage; @@ -95,6 +96,11 @@ public class ProfilesViewController { openButton.setOnAction(event -> openProfile(profileName, false)); openButton.setDisable(isCurrent); buttonBox.getChildren().add(openButton); + Button viewFilesButton = new Button("View Files"); + viewFilesButton.setOnAction(event -> { + PerfinApp.instance.getHostServices().showDocument(Profile.getDir(profileName).toUri().toString()); + }); + buttonBox.getChildren().add(viewFilesButton); Button deleteButton = new Button("Delete"); deleteButton.setOnAction(event -> deleteProfile(profileName)); buttonBox.getChildren().add(deleteButton); diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java index 8cfe6b0..1ea948e 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java @@ -27,6 +27,8 @@ import static com.andrewlalis.perfin.PerfinApp.router; public class TransactionViewController { private Transaction transaction; + @FXML public Label titleLabel; + @FXML public Label amountLabel; @FXML public Label timestampLabel; @FXML public Label descriptionLabel; @@ -41,6 +43,7 @@ public class TransactionViewController { public void setTransaction(Transaction transaction) { this.transaction = transaction; if (transaction == null) return; + titleLabel.setText("Transaction #" + transaction.getId()); amountLabel.setText(CurrencyUtil.formatMoney(transaction.getAmount(), transaction.getCurrency())); timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp())); descriptionLabel.setText(transaction.getDescription()); diff --git a/src/main/java/com/andrewlalis/perfin/view/ImageCache.java b/src/main/java/com/andrewlalis/perfin/view/ImageCache.java new file mode 100644 index 0000000..cd2051c --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/view/ImageCache.java @@ -0,0 +1,40 @@ +package com.andrewlalis.perfin.view; + +import javafx.scene.image.Image; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ImageCache { + public static final ImageCache instance = new ImageCache(); + + private final Map images = new ConcurrentHashMap<>(); + + public Image get(String resource, double width, double height, boolean preserveRatio, boolean smooth) { + final String cacheKey = getCacheKey(resource, width, height, preserveRatio, smooth); + Image stored = images.get(cacheKey); + if (stored != null) return stored; + try (var in = ImageCache.class.getResourceAsStream(resource)) { + if (in == null) throw new IOException("Could not load resource " + resource); + Image img = new Image(in, width, height, preserveRatio, smooth); + images.put(cacheKey, img); + return img; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private String getCacheKey(String resource, double width, double height, boolean preserveRatio, boolean smooth) { + return resource + "_" + + "W" + width + "_" + + "H" + height + "_" + + "PR-" + preserveRatio + "_" + + "S-" + smooth; + } + + public static Image getLogo64() { + return instance.get("/images/perfin-logo_64.png", 64, 64, true, true); + } +} diff --git a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java index 1e55ec9..3d2e476 100644 --- a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java +++ b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java @@ -26,6 +26,7 @@ public class StartupSplashScreen extends Stage implements Consumer { setTitle("Starting Perfin..."); setResizable(false); initStyle(StageStyle.UNDECORATED); + getIcons().add(ImageCache.getLogo64()); setScene(buildScene()); setOnShowing(event -> runTasks()); diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java new file mode 100644 index 0000000..b4f56c7 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountHistoryItemTile.java @@ -0,0 +1,57 @@ +package com.andrewlalis.perfin.view.component; + +import com.andrewlalis.perfin.data.AccountHistoryItemRepository; +import com.andrewlalis.perfin.data.util.CurrencyUtil; +import com.andrewlalis.perfin.data.util.DateUtil; +import com.andrewlalis.perfin.model.AccountEntry; +import com.andrewlalis.perfin.model.BalanceRecord; +import com.andrewlalis.perfin.model.history.AccountHistoryItem; +import javafx.scene.Node; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +/** + * A tile that shows a brief bit of information about an account history item. + */ +public class AccountHistoryItemTile extends BorderPane { + public AccountHistoryItemTile(AccountHistoryItem item, AccountHistoryItemRepository repo) { + setStyle(""" + -fx-border-color: lightgray; + -fx-border-radius: 5px; + -fx-padding: 5px; + """); + + Label timestampLabel = new Label(DateUtil.formatUTCAsLocalWithZone(item.getTimestamp())); + timestampLabel.setStyle("-fx-font-size: small;"); + setTop(timestampLabel); + setCenter(switch (item.getType()) { + case TEXT -> buildTextItem(repo.getTextItem(item.getId())); + case ACCOUNT_ENTRY -> buildAccountEntryItem(repo.getAccountEntryItem(item.getId())); + case BALANCE_RECORD -> buildBalanceRecordItem(repo.getBalanceRecordItem(item.getId())); + }); + } + + private Node buildTextItem(String text) { + return new TextFlow(new Text(text)); + } + + private Node buildAccountEntryItem(AccountEntry entry) { + Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency())); + Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId()); + return new TextFlow( + new Text("Entry added with value of "), + amountText, + new Text(", linked with "), + transactionLink, + new Text(".") + ); + } + + private Node buildBalanceRecordItem(BalanceRecord balanceRecord) { + Text amountText = new Text(CurrencyUtil.formatMoney(balanceRecord.getBalance(), balanceRecord.getCurrency())); + return new TextFlow(new Text("Balance record added with value of "), amountText); + } +} 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 f211dce..2263dee 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java @@ -2,6 +2,7 @@ package com.andrewlalis.perfin.view.component; import com.andrewlalis.perfin.model.Attachment; import com.andrewlalis.perfin.model.Profile; +import com.andrewlalis.perfin.view.ImageCache; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -43,13 +44,7 @@ public class AttachmentPreview extends BorderPane { } } if (showDocIcon) { - try (var in = AttachmentPreview.class.getResourceAsStream("/images/doc-icon.png")) { - if (in == null) throw new NullPointerException("Missing /images/doc-icon.png resource."); - Image img = new Image(in, IMAGE_SIZE, IMAGE_SIZE, true, true); - contentContainer.setCenter(new ImageView(img)); - } catch (IOException e) { - throw new RuntimeException(e); - } + contentContainer.setCenter(new ImageView(ImageCache.instance.get("/images/doc-icon.png", 64, 64, true, true))); } BorderPane hoverIndicatorPane = new BorderPane(); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4ceaa78..3936074 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -7,10 +7,10 @@ module com.andrewlalis.perfin { requires com.fasterxml.jackson.databind; - requires java.sql; - requires com.github.f4b6a3.ulid; + requires java.sql; + exports com.andrewlalis.perfin to javafx.graphics; exports com.andrewlalis.perfin.view to javafx.graphics; exports com.andrewlalis.perfin.model to javafx.graphics; diff --git a/src/main/resources/account-view.fxml b/src/main/resources/account-view.fxml index 461a4f8..dab0c8b 100644 --- a/src/main/resources/account-view.fxml +++ b/src/main/resources/account-view.fxml @@ -14,37 +14,53 @@
- - - - - -
- - -