From ce78df559e246f53d5560ab1923dd0ea86e08081 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Tue, 9 Jan 2024 12:34:06 -0500 Subject: [PATCH] Added help pages, styled text implementation, and improved splash screen. --- design/splash-screen.svg | 71 +++++-- .../com/andrewlalis/perfin/PerfinApp.java | 28 +-- .../perfin/control/MainViewController.java | 48 ++++- .../control/ProfilesViewController.java | 24 +-- .../perfin/view/StartupSplashScreen.java | 9 +- .../view/component/ScrollPaneRouterView.java | 23 +++ .../perfin/view/component/StyledText.java | 181 ++++++++++++++++++ .../resources/help-pages/accounts-view.fxml | 35 ++++ .../help-pages/adding-a-transaction.fxml | 65 +++++++ .../help-pages/adding-an-account.fxml | 45 +++++ src/main/resources/help-pages/help-test.fxml | 15 -- src/main/resources/help-pages/home.fxml | 42 ++-- src/main/resources/help-pages/profiles.fxml | 28 +++ .../help-pages/transactions-view.fxml | 23 +++ src/main/resources/images/splash-screen.png | Bin 5611 -> 15168 bytes src/main/resources/main-view.fxml | 64 +++++-- src/main/resources/profiles-view.fxml | 12 +- 17 files changed, 592 insertions(+), 121 deletions(-) create mode 100644 src/main/java/com/andrewlalis/perfin/view/component/ScrollPaneRouterView.java create mode 100644 src/main/java/com/andrewlalis/perfin/view/component/StyledText.java create mode 100644 src/main/resources/help-pages/accounts-view.fxml create mode 100644 src/main/resources/help-pages/adding-a-transaction.fxml create mode 100644 src/main/resources/help-pages/adding-an-account.fxml delete mode 100644 src/main/resources/help-pages/help-test.fxml create mode 100644 src/main/resources/help-pages/profiles.fxml create mode 100644 src/main/resources/help-pages/transactions-view.fxml diff --git a/design/splash-screen.svg b/design/splash-screen.svg index 580ab2f..20472aa 100644 --- a/design/splash-screen.svg +++ b/design/splash-screen.svg @@ -26,9 +26,9 @@ inkscape:pagecheckerboard="1" inkscape:deskcolor="#505050" inkscape:document-units="px" - inkscape:zoom="2.0863221" - inkscape:cx="143.55406" - inkscape:cy="122.22466" + inkscape:zoom="1.4752525" + inkscape:cx="125.40226" + inkscape:cy="94.899008" inkscape:window-width="1920" inkscape:window-height="1025" inkscape:window-x="1080" @@ -36,6 +36,11 @@ inkscape:window-maximized="1" inkscape:current-layer="layer1" /> + sodipodi:nodetypes="cccscccccccccccccccccccscsc" />PerFinPersonal Finance diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index 1519681..ea3918f 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -7,6 +7,7 @@ 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 com.andrewlalis.perfin.view.component.ScrollPaneRouterView; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; @@ -38,7 +39,7 @@ public class PerfinApp extends Application { * A router that controls which help page is being viewed in the side-pane. * Certain user actions may cause this router to navigate to certain pages. */ - public static final SceneRouter helpRouter = new SceneRouter(new AnchorPaneRouterView(true)); + public static final SceneRouter helpRouter = new SceneRouter(new ScrollPaneRouterView()); public static void main(String[] args) { launch(args); @@ -75,23 +76,24 @@ public class PerfinApp extends Application { }); } - private static void mapResourceRoute(String route, String resource) { - router.map(route, PerfinApp.class.getResource(resource)); - } - private static void defineRoutes(Consumer msgConsumer) { msgConsumer.accept("Initializing application views."); Platform.runLater(() -> { - mapResourceRoute("accounts", "/accounts-view.fxml"); - mapResourceRoute("account", "/account-view.fxml"); - mapResourceRoute("edit-account", "/edit-account.fxml"); - mapResourceRoute("transactions", "/transactions-view.fxml"); - mapResourceRoute("create-transaction", "/create-transaction.fxml"); - mapResourceRoute("create-balance-record", "/create-balance-record.fxml"); + // App pages. + router.map("accounts", PerfinApp.class.getResource("/accounts-view.fxml")); + router.map("account", PerfinApp.class.getResource("/account-view.fxml")); + router.map("edit-account", PerfinApp.class.getResource("/edit-account.fxml")); + router.map("transactions", PerfinApp.class.getResource("/transactions-view.fxml")); + router.map("create-transaction", PerfinApp.class.getResource("/create-transaction.fxml")); + router.map("create-balance-record", PerfinApp.class.getResource("/create-balance-record.fxml")); - // Map help pages. - helpRouter.map("help-test", PerfinApp.class.getResource("/help-pages/help-test.fxml")); + // Help pages. helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml")); + helpRouter.map("accounts", PerfinApp.class.getResource("/help-pages/accounts-view.fxml")); + helpRouter.map("adding-an-account", PerfinApp.class.getResource("/help-pages/adding-an-account.fxml")); + helpRouter.map("transactions", PerfinApp.class.getResource("/help-pages/transactions-view.fxml")); + helpRouter.map("adding-a-transaction", PerfinApp.class.getResource("/help-pages/adding-a-transaction.fxml")); + helpRouter.map("profiles", PerfinApp.class.getResource("/help-pages/profiles.fxml")); }); } diff --git a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java index 1872308..1a55118 100644 --- a/src/main/java/com/andrewlalis/perfin/control/MainViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/MainViewController.java @@ -3,12 +3,13 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView; import com.andrewlalis.perfin.view.BindingUtil; import com.andrewlalis.perfin.view.ProfilesStage; +import com.andrewlalis.perfin.view.component.ScrollPaneRouterView; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; import static com.andrewlalis.perfin.PerfinApp.helpRouter; import static com.andrewlalis.perfin.PerfinApp.router; @@ -19,7 +20,8 @@ public class MainViewController { @FXML public Button showManualButton; @FXML public Button hideManualButton; - @FXML public VBox manualVBox; + @FXML public BorderPane helpPane; + @FXML public Button helpBackButton; @FXML public void initialize() { AnchorPaneRouterView routerView = (AnchorPaneRouterView) router.getView(); @@ -41,15 +43,26 @@ public class MainViewController { router.navigate("accounts"); // Initialize the help manual components. - manualVBox.managedProperty().bind(manualVBox.visibleProperty()); - manualVBox.setVisible(false); + helpPane.managedProperty().bind(helpPane.visibleProperty()); + helpPane.setVisible(false); showManualButton.managedProperty().bind(showManualButton.visibleProperty()); - showManualButton.visibleProperty().bind(manualVBox.visibleProperty().not()); + showManualButton.visibleProperty().bind(helpPane.visibleProperty().not()); hideManualButton.managedProperty().bind(hideManualButton.visibleProperty()); - hideManualButton.visibleProperty().bind(manualVBox.visibleProperty()); + hideManualButton.visibleProperty().bind(helpPane.visibleProperty()); - AnchorPaneRouterView helpRouterView = (AnchorPaneRouterView) helpRouter.getView(); - manualVBox.getChildren().add(helpRouterView.getAnchorPane()); + helpBackButton.managedProperty().bind(helpBackButton.visibleProperty()); + helpRouter.currentRouteProperty().addListener((observable, oldValue, newValue) -> { + helpBackButton.setVisible(helpRouter.getHistory().canGoBack()); + }); + helpBackButton.setOnAction(event -> helpRouter.navigateBack()); + + ScrollPaneRouterView helpRouterView = (ScrollPaneRouterView) helpRouter.getView(); + ScrollPane helpRouterScrollPane = helpRouterView.getScrollPane(); + helpRouterScrollPane.setMinWidth(200.0); + helpRouterScrollPane.setMaxWidth(400.0); + helpRouterScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS); + helpRouterScrollPane.getStyleClass().addAll("padding-extra"); + helpPane.setCenter(helpRouterScrollPane); helpRouter.navigate("home"); } @@ -77,10 +90,25 @@ public class MainViewController { } @FXML public void showManual() { - manualVBox.setVisible(true); + helpPane.setVisible(true); } @FXML public void hideManual() { - manualVBox.setVisible(false); + helpPane.setVisible(false); + } + + @FXML public void helpViewHome() { + helpRouter.getHistory().clear(); + helpRouter.navigate("home"); + } + + @FXML public void helpViewAccounts() { + helpRouter.getHistory().clear(); + helpRouter.navigate("accounts"); + } + + @FXML public void helpViewTransactions() { + helpRouter.getHistory().clear(); + helpRouter.navigate("transactions"); } } diff --git a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java index 1fb72eb..edeb452 100644 --- a/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/ProfilesViewController.java @@ -5,8 +5,8 @@ import com.andrewlalis.perfin.data.ProfileLoadException; import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.view.ProfilesStage; -import javafx.beans.binding.BooleanExpression; -import javafx.beans.property.BooleanProperty; +import com.andrewlalis.perfin.view.component.validation.ValidationApplier; +import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; @@ -16,6 +16,8 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; @@ -24,22 +26,16 @@ import java.util.List; import static com.andrewlalis.perfin.PerfinApp.router; public class ProfilesViewController { + private static final Logger log = LoggerFactory.getLogger(ProfilesViewController.class); + @FXML public VBox profilesVBox; @FXML public TextField newProfileNameField; - @FXML public Text newProfileNameErrorLabel; @FXML public Button addProfileButton; @FXML public void initialize() { - BooleanExpression newProfileNameValid = BooleanProperty.booleanExpression(newProfileNameField.textProperty() - .map(text -> ( - text != null && - !text.isBlank() && - Profile.validateName(text) && - !Profile.getAvailableProfiles().contains(text) - ))); - newProfileNameErrorLabel.managedProperty().bind(newProfileNameErrorLabel.visibleProperty()); - newProfileNameErrorLabel.visibleProperty().bind(newProfileNameValid.not().and(newProfileNameField.textProperty().isNotEmpty())); - newProfileNameErrorLabel.wrappingWidthProperty().bind(newProfileNameField.widthProperty()); + var newProfileNameValid = new ValidationApplier<>(new PredicateValidator() + .addPredicate(s -> s == null || s.isBlank() || Profile.validateName(s), "Profile name should consist of only lowercase numbers.") + ).attachToTextField(newProfileNameField); addProfileButton.disableProperty().bind(newProfileNameValid.not()); refreshAvailableProfiles(); @@ -106,7 +102,7 @@ public class ProfilesViewController { } private boolean openProfile(String name, boolean showPopup) { - System.out.println("Opening profile: " + name); + log.info("Opening profile \"{}\".", name); try { Profile.load(name); ProfilesStage.closeView(); diff --git a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java index 4714cb9..526951d 100644 --- a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java +++ b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java @@ -62,10 +62,15 @@ public class StartupSplashScreen extends Stage implements Consumer { private void runTasks() { Thread.ofVirtual().start(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } for (var task : tasks) { try { task.accept(this); - Thread.sleep(100); + Thread.sleep(500); } catch (Exception e) { accept("Startup failed: " + e.getMessage()); e.printStackTrace(System.err); @@ -80,7 +85,7 @@ public class StartupSplashScreen extends Stage implements Consumer { } accept("Startup successful!"); try { - Thread.sleep(500); + Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/ScrollPaneRouterView.java b/src/main/java/com/andrewlalis/perfin/view/component/ScrollPaneRouterView.java new file mode 100644 index 0000000..cf6b1ea --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/view/component/ScrollPaneRouterView.java @@ -0,0 +1,23 @@ +package com.andrewlalis.perfin.view.component; + +import com.andrewlalis.javafx_scene_router.RouterView; +import javafx.scene.Parent; +import javafx.scene.control.ScrollPane; + +public class ScrollPaneRouterView implements RouterView { + private final ScrollPane scrollPane = new ScrollPane(); + + public ScrollPaneRouterView() { + scrollPane.setFitToHeight(true); + scrollPane.setFitToWidth(true); + } + + @Override + public void showRouteNode(Parent node) { + scrollPane.setContent(node); + } + + public ScrollPane getScrollPane() { + return scrollPane; + } +} diff --git a/src/main/java/com/andrewlalis/perfin/view/component/StyledText.java b/src/main/java/com/andrewlalis/perfin/view/component/StyledText.java new file mode 100644 index 0000000..b6fdfd8 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/view/component/StyledText.java @@ -0,0 +1,181 @@ +package com.andrewlalis.perfin.view.component; + +import com.andrewlalis.perfin.PerfinApp; +import javafx.beans.DefaultProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.property.StringPropertyBase; +import javafx.geometry.Insets; +import javafx.scene.AccessibleAttribute; +import javafx.scene.control.Hyperlink; +import javafx.scene.layout.Border; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import java.util.ArrayList; +import java.util.List; + +import static com.andrewlalis.perfin.PerfinApp.helpRouter; + +/** + * A component that renders markdown-ish text as a series of TextFlow elements, + * styled according to some basic styles. + */ +@DefaultProperty("text") +public class StyledText extends VBox { + private StringProperty text; + private boolean initialized = false; + + public final void setText(String value) { + if (value == null) value = ""; + textProperty().set(value); + } + + public final String getText() { + return text == null ? "" : text.get(); + } + + public final StringProperty textProperty() { + if (text == null) { + text = new StringPropertyBase("") { + @Override public Object getBean() { return StyledText.this; } + @Override public String getName() { return "text"; } + @Override public void invalidated() { + notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT); + } + }; + } + return text; + } + + @Override + protected void layoutChildren() { + if (!initialized) { + String s = getText(); + getChildren().clear(); + getChildren().addAll(renderText(s)); + getStyleClass().add("spacing-extra"); + initialized = true; + } + super.layoutChildren(); + } + + private List renderText(String text) { + return new TextFlowBuilder().build(text); + } + + private static class TextFlowBuilder { + private final List flows = new ArrayList<>(); + private int idx = 0; + private final StringBuilder currentRun = new StringBuilder(); + private TextFlow currentParagraph; + + public List build(String text) { + flows.clear(); + idx = 0; + currentRun.setLength(0); + currentParagraph = new TextFlow(); + + while (idx < text.length()) { + if (text.startsWith("**", idx)) { + parseStyledText(text, "**", "bold-text"); + } else if (text.startsWith("*", idx)) { + parseStyledText(text, "*", "italic-text"); + } else if (text.startsWith("`", idx)) { + parseStyledText(text, "`", "mono-font"); + } else if (text.startsWith(" -- ", idx)) { + parsePageBreak(text); + } else if (text.startsWith("[", idx)) { + parseLink(text); + } else if (text.startsWith("#", idx) && (idx == 0 || (idx > 0 && text.charAt(idx - 1) == ' '))) { + parseHeader(text); + } else { + currentRun.append(text.charAt(idx)); + idx++; + } + } + appendTextIfPresent(); + appendParagraphIfPresent(); + return flows; + } + + private void parsePageBreak(String text) { + appendTextIfPresent(); + appendParagraphIfPresent(); + while (text.charAt(idx) == ' ') idx++; + while (text.charAt(idx) == '-') idx++; + while (text.charAt(idx) == ' ') idx++; + } + + private void parseStyledText(String text, String marker, String styleClass) { + appendTextIfPresent(); + int endIdx = text.indexOf(marker, idx + marker.length()); + Text textItem = new Text(text.substring(idx + marker.length(), endIdx)); + textItem.getStyleClass().add(styleClass); + currentParagraph.getChildren().add(textItem); + idx = endIdx + marker.length(); + } + + private void parseLink(String text) { + appendTextIfPresent(); + int labelEndIdx = text.indexOf(']', idx); + String label = text.substring(idx + 1, labelEndIdx); + idx = labelEndIdx + 1; + final String link; + if (text.charAt(labelEndIdx + 1) == '(') { + int linkEndIdx = text.indexOf(')', labelEndIdx + 2); + link = text.substring(labelEndIdx + 2, linkEndIdx); + idx = linkEndIdx + 1; + } else { + link = null; + } + Hyperlink hyperlink = new Hyperlink(label); + if (link != null) { + if (link.startsWith("http")) { + hyperlink.setOnAction(event -> PerfinApp.instance.getHostServices().showDocument(link)); + } else if (link.startsWith("help:")) { + hyperlink.setOnAction(event -> helpRouter.navigate(link.substring(5).strip())); + } + } + hyperlink.setBorder(Border.EMPTY); + hyperlink.setPadding(new Insets(0, 0, 0, 0)); + currentParagraph.getChildren().add(hyperlink); + } + + private void parseHeader(String text) { + appendTextIfPresent(); + appendParagraphIfPresent(); + int size = 0; + while (text.charAt(idx) == '#') { + idx++; + size++; + } + int endIdx = text.indexOf("#".repeat(size), idx); + Text header = new Text(text.substring(idx, endIdx).strip()); + idx = endIdx + size; + while (text.charAt(idx) == ' ') idx++; + String styleClass = switch(size) { + case 1 -> "large-font"; + case 2 -> "largest-font"; + default -> "largest-font"; + }; + header.getStyleClass().addAll(styleClass, "bold-text"); + currentParagraph.getChildren().add(header); + appendParagraphIfPresent(); + } + + private void appendTextIfPresent() { + if (!currentRun.isEmpty()) { + currentParagraph.getChildren().add(new Text(currentRun.toString())); + currentRun.setLength(0); + } + } + + private void appendParagraphIfPresent() { + if (!currentParagraph.getChildren().isEmpty()) { + flows.add(currentParagraph); + currentParagraph = new TextFlow(); + } + } + } +} diff --git a/src/main/resources/help-pages/accounts-view.fxml b/src/main/resources/help-pages/accounts-view.fxml new file mode 100644 index 0000000..dc1b590 --- /dev/null +++ b/src/main/resources/help-pages/accounts-view.fxml @@ -0,0 +1,35 @@ + + + + + + + ## The Accounts View ## + In the *Accounts view*, you'll see an overview of all the active + accounts in your *Perfin* profile. + -- + To add a new account, simply click on **Add an Account** from the menu + at the top. [Read about how to add an account here.](help:adding-an-account) + -- + Additionally, the top menu also shows an overview of the derived total + balance for each currency you own. This adds up the balances of all + accounts of like currency, and gives you a total of each. + -- + Accounts are, by default, ordered according to the most recent activity. + That means that your most active accounts will appear *first* in this + page, and your least active accounts will appear *last*. + + # Account Tiles # + Each account's tile displays some basic information about the account + up-front, like its name, number, balance, and type. Click on a tile to + navigate to that account's page for more detailed information, and to + make changes to the account. + -- + The *current balance* shown in each account tile is derived from the + account's last-known **balance record**, and all transactions that have + happened between then and now. + + diff --git a/src/main/resources/help-pages/adding-a-transaction.fxml b/src/main/resources/help-pages/adding-a-transaction.fxml new file mode 100644 index 0000000..0994e0d --- /dev/null +++ b/src/main/resources/help-pages/adding-a-transaction.fxml @@ -0,0 +1,65 @@ + + + + + + + ## Adding a Transaction ## + When you're adding a new transaction to your *Perfin* profile, there are + some details that you should be aware of. + + # Timestamp # + The timestamp is the date and time at which the transaction took place, + in your local time zone. Generally, it's encouraged to set this as the + real timestamp at which the transaction took place (the time shown on a + receipt or invoice, for example), rather than the timestamp provided by + your financial institution once the payment has been processed. + -- + It's formatted as `yyyy-mm-dd HH:MM:SS`, so for example, November 14th, + 2015 at 4:15pm would be written as `2015-11-14 16:15:00`. You can omit + the seconds if you like, however. + -- + Also note that you may not enter timestamps from the future; it just + doesn't make sense to do so. + + # Amount # + The total amount of the transaction, as a positive decimal value. This + is the final amount which you've paid or received, including any tax, + tips, or transaction fees. + + # Currency # + The currency of the transaction. This should be the same currency as the + account(s) that the transaction is linked to. + + # Linked Debit and Credit Accounts # + Every transaction, for it to mean something, needs to be linked to one + (or sometimes two) of your accounts. A transaction's impact on an + account depends on whether the transaction is being treated as a **Debit** + or a **Credit** on the account. + -- + The account linked as **Debit** is the one whose assets will + **increase** as a result of the transaction. Some common examples of + transactions with debit-linked accounts include deposits to checking + or savings accounts, or refunds to credit cards. + -- + The account linked as **Credit** is the one whose assets will + **decrease** as a result of the transaction. Some common examples of + transactions with credit-linked accounts include payments or purchases + with a checking or savings account, or a credit card. + -- + In short, if your account is *gaining money*, link it under **debit**. + If your account is *losing money*, link it under **credit**. + -- + For transfers between two accounts that are both tracked in *Perfin*, + the *sending* account should be linked under **credit**, and the + *receiving* account under **debit**. + + # Attachments # + Often, you'll have a receipt, invoice, bank statement, or some other + document which acts as proof of a transaction. You can attach files to + a transaction to save those files with it, as a reference. The files + will be copied to your *Perfin* profile. + + diff --git a/src/main/resources/help-pages/adding-an-account.fxml b/src/main/resources/help-pages/adding-an-account.fxml new file mode 100644 index 0000000..a41288c --- /dev/null +++ b/src/main/resources/help-pages/adding-an-account.fxml @@ -0,0 +1,45 @@ + + + + + + + ## Adding an Account ## + When adding an account, you'll need to provide some basic information + about the account so that Perfin can integrate it with its automatic + balance calculations and other functions. + + # Name # + The name of the account is just a bit of text that you can use to + identify the account from the rest of yours. Make sure it's unique, and + to-the-point, since the account's name is used elsewhere in the app to + refer to the account. + + # Number # + The number of the account is the unique account number provided by your + financial institution. For checking and savings accounts, it'll be the + account number provided by your bank, and for credit cards, it'll be the + credit card's number. + + # Currency # + The currency is pretty self-explanatory; simply select the currency that + your account uses. Perfin uses 3-character *currency codes* quite often, + and you can read about them [here](https://en.wikipedia.org/wiki/ISO_4217#List_of_currency_codes). + + # Account Type # + The account's type determines how certain balance calculations and + properties of the account work. For instance, a **Credit Card** + account's balance is interpreted to be the amount you owe to the card's + provider (the amount you need to pay off), while a **Checking** + account's balance is naturally the amount of money in your account. + + # Initial Balance # + In order to accurately compute your account's balance (without needing + to check back every hour with your financial institution), Perfin needs + to know what your account's starting balance is. This way, it can use + that balance, combined with any transactions you add, to tell you your + balance at any moment in time. + + diff --git a/src/main/resources/help-pages/help-test.fxml b/src/main/resources/help-pages/help-test.fxml deleted file mode 100644 index a01b7aa..0000000 --- a/src/main/resources/help-pages/help-test.fxml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - This is a testing help page, which is meant to be shown only for - testing the help system and navigation. - - - diff --git a/src/main/resources/help-pages/home.fxml b/src/main/resources/help-pages/home.fxml index cd3a519..6a4ff20 100644 --- a/src/main/resources/help-pages/home.fxml +++ b/src/main/resources/help-pages/home.fxml @@ -1,29 +1,35 @@ - + - - - diff --git a/src/main/resources/help-pages/profiles.fxml b/src/main/resources/help-pages/profiles.fxml new file mode 100644 index 0000000..9f83d5d --- /dev/null +++ b/src/main/resources/help-pages/profiles.fxml @@ -0,0 +1,28 @@ + + + + + + + ## Profiles ## + In *Perfin*, all the accounts, transactions, attachments, bank + statements, and other financial data, are stored in a *profile*. You can + think of it as a save file. It's a single folder that encapsulates all + your data. + -- + Perfin uses profiles to make it easy to work with multiple financial + portfolios, export and import bulk data, and to let *you*, the user, + access your data directly, as opposed to hiding it in some proprietary + file format. + -- + A running Perfin program always has one profile open at a time. You can + change the active profile by clicking **Profiles** from the app's top + menu, and then clicking **Open** next to the profile you'd like to use. + -- + By default, when you first start Perfin, a profile named *default* is + created for you to use. Most of the time, you won't need to worry about + using other profiles, but if you do, now you know how. + + diff --git a/src/main/resources/help-pages/transactions-view.fxml b/src/main/resources/help-pages/transactions-view.fxml new file mode 100644 index 0000000..a627185 --- /dev/null +++ b/src/main/resources/help-pages/transactions-view.fxml @@ -0,0 +1,23 @@ + + + + + + + ## The Transactions View ## + In the *Transactions view*, you're shown a list of all transactions that + have been added to your *Perfin* profile, along with some controls for + navigating this list. + -- + The transactions are ordered, by default, according to their timestamp, + with recent transactions appearing before older ones. + -- + Simply click on a transaction to view its details or make changes. + -- + To add a new transaction, click **Add Transaction** in the top menu. You + can read more about adding a transaction [here](help:adding-a-transaction). + + diff --git a/src/main/resources/images/splash-screen.png b/src/main/resources/images/splash-screen.png index 76d1e27097028d80e3da232d373298abb35e5316..1cb68f2276772d536a3751ac8ccb5d685dc5ceb4 100644 GIT binary patch literal 15168 zcmX9_1z23Y5-k*WDDJdCad)R!@nXf@-QC@ayBCMWp|})xcXxM}V(;$%z7O5KnItol zk#n-)Kjb8k5%3W{eE5JY`CU};!v~01;O{{=7~p3=R^vAC3*PR#y5olrC_UhBhymLY zBj80mCowfAC0i3GSA7TL53a7R4CXeLj)wYn#tgO&rWxma_#ZxyevlLuR(8ugYj;gG za&KBV)^ z+`!yV>DFEibrFwb3H2Q}S1&P+(zSdt$uZ0EookMt2-HIB>?1glUo4@mQY@FnWWiWEqp+EAo`^+V-{^7Dq&-QQr}GMe9h z0k29M+pkzq`Y~JcvH~s^MLXO25OoT013WED+QE5#Ma$o+A9D;4LyCT=g!H3BphM}; z{J?xnN^t!396_{ZQwWJ;LYj4UXyQs20hw+R8C(>g@!B|DAXFX-7X~b&KWhiR@?yft zDI*voGv|-*x!5dp%_Ku)2*_sKGtv((pqd&G)b?QZCx2xWYjc%6k(Oxfv_VWkNcS^# z%lwmy7;6~#sVy$$NMFZ0I=Onsv@<7c1{smU8}~=+z`TIUsqXcLrFWNdri& zxGn_ojk`CR+a&1Vl^k2Erll0DbM2IUZGKXLR78cKgcpCvJs7)>x#bzIL{jQ`j90cS zBOmf`foPFrSB~4wQP9zZ(KvqnuImn!A9bdaBZ=;kF`c2xh;FGKo@^8donrWJ8n_5Z z5^ipZ#;G)<2P@2d7qgg9&~m_*-C|yiCK^~zB2Ct8Lr3f5Y$l_poiM_(_0T;>i*L=p zeuA}_yAz~^weDKLT2s`#IF61g3tc>;V}1H_N-YAcH==`%9euCp`xDDA_PkE@SxXgL zrQ%?zx%q8IRBLfIH)@bYJw%JS$U+cKUvXQq&^F3Ww_+IJz7=2b(yTg)6`Y-sv%2IA zxQHm7WroZ&*Oe9X1pg;_XMG1fVEi6b$-=#YYu|e7P8HEU%tYwvS+9V|O&CDx6ohkY z>OSlY>&5| z=)a9wuY1^KL%5#!94A8`6zBd4PCjk1K@1hCj6nVpDuou*fP0_l)+5?hVXc+vZ`C2f zJ4=r8wTX~-BGiJH6!J6NH_eyRE4XM5tK~2D-~XiWnB$Apl$9d&5D$|72?5Vf?|7q^ zZn;uTJG8G~^a|PtQK;@wmP3z@{%^E&lXC(pv1Pk#n^q0%H|u^GRA^r6bB_CvzCjZ* zuz2aV*GWDy)#kzljbwV_!g-x_%oY^l6u1HL|HjY!dWu+`>yIr-3tMAqiaW$_((;_h zv0(jgZ|3LR_+qxq_SrHk;Wts=l)BF1E3d|cP-;lwWbM6uLuR}yD8{8Jn*InAt40`n z9V~S8y7|v)`h=jqXAPN5&Bbknqfx(ZV=kh5kVufB<9}lH&$Gi=Pt7Z|`B+eo#*wJB zL@Pqd3Ov0mV|xILirh90w;c&t4n=yQoAC0fOxi6QzazI&%XD=*yzd0>FB-%%ic}NZLwf zSWY7pJy_X*g|=%xt4^x+{?B`5&vB)hwrqXi^);BdSKf?->+x$b8duN$JJ!xu+%oI# zCG`}&R+tFZ;}4VUmu|g}cCDYm3f5kC<`VxMjCaud?KzjWc!onT(QDvd3`!I&DE;&X zr`els>Gp&Uk)a;1#R2c$xBFfk{FVw8!m6tnl&rZ9O!rMJI;~1}k6iiB;>9 z4pr2`vshgc~x

Nh+^c$^ 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 NB*o-JE5GRl{14wef>i(j delta 5568 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 St4r+w_*t6TUTQM-PW~T{7lxex diff --git a/src/main/resources/main-view.fxml b/src/main/resources/main-view.fxml index 318e645..a68e430 100644 --- a/src/main/resources/main-view.fxml +++ b/src/main/resources/main-view.fxml @@ -5,30 +5,52 @@ - - - -