diff --git a/pom.xml b/pom.xml index 8504fba..44a2705 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ 21 UTF-8 21.0.1 + com.andrewlalis.perfin.PerfinApp @@ -60,6 +61,46 @@ com.andrewlalis.perfin.PerfinApp + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + + com.andrewlalis.perfin.PerfinApp + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.0 + + + package + + copy-dependencies + + + ${project.build.directory}/lib + + + + \ No newline at end of file diff --git a/run-jar.sh b/run-jar.sh new file mode 100755 index 0000000..ab86ff2 --- /dev/null +++ b/run-jar.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +shouldBuild=0 +for i in "$@" ; do + if [[ $i == "build" ]] ; then + shouldBuild=1 + break + fi +done + +if [ $shouldBuild == 1 ]; then + mvn clean + mvn package +fi + +java \ + --add-modules=javafx.controls,com.andrewlalis.javafx_scene_router \ + --module-path=target/lib/ \ + -jar target/perfin-*-jar-with-dependencies.jar diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index fe678a0..e9d6061 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -2,12 +2,17 @@ package com.andrewlalis.perfin; import com.andrewlalis.javafx_scene_router.AnchorPaneRouterView; import com.andrewlalis.javafx_scene_router.SceneRouter; -import com.andrewlalis.perfin.view.SplashScreenStage; +import com.andrewlalis.perfin.model.Profile; +import com.andrewlalis.perfin.view.StartupSplashScreen; import javafx.application.Application; +import javafx.application.Platform; import javafx.scene.Scene; import javafx.stage.Stage; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.function.Consumer; /** * The class from which the JavaFX-based application starts. @@ -26,33 +31,57 @@ public class PerfinApp extends Application { @Override public void start(Stage stage) { - SplashScreenStage splashStage = new SplashScreenStage("Loading", SceneUtil.load("/startup-splash-screen.fxml")); - splashStage.show(); - defineRoutes(); - initMainScreen(stage); - splashStage.stateProperty().addListener((v, oldState, state) -> { - if (state == SplashScreenStage.State.DONE) stage.show(); - if (state == SplashScreenStage.State.ERROR) System.out.println("ERROR!"); - }); + var splashScreen = new StartupSplashScreen(List.of( + PerfinApp::defineRoutes, + PerfinApp::initAppDir, + c -> initMainScreen(stage, c), + PerfinApp::loadProfile + )); + splashScreen.showAndWait(); + if (splashScreen.isStartupSuccessful()) { + stage.show(); + } } - private void initMainScreen(Stage stage) { - stage.hide(); - Scene mainViewScene = SceneUtil.load("/main-view.fxml"); - stage.setScene(mainViewScene); - stage.setTitle("Perfin"); + private void initMainScreen(Stage stage, Consumer msgConsumer) throws Exception { + msgConsumer.accept("Initializing main screen."); + Platform.runLater(() -> { + stage.hide(); + Scene mainViewScene = SceneUtil.load("/main-view.fxml"); + stage.setScene(mainViewScene); + stage.setTitle("Perfin"); + }); } private static void mapResourceRoute(String route, String resource) { router.map(route, PerfinApp.class.getResource(resource)); } - private static void defineRoutes() { - 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("transaction", "/transaction-view.fxml"); + private static void defineRoutes(Consumer msgConsumer) throws Exception { + 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"); + }); + } + + private static void initAppDir(Consumer msgConsumer) throws Exception { + msgConsumer.accept("Validating application files."); + if (Files.notExists(APP_DIR)) { + msgConsumer.accept(APP_DIR + " doesn't exist yet. Creating it now."); + Files.createDirectory(APP_DIR); + } else if (Files.exists(APP_DIR) && Files.isRegularFile(APP_DIR)) { + msgConsumer.accept(APP_DIR + " is a file, when it should be a directory. Deleting it and creating new directory."); + Files.delete(APP_DIR); + Files.createDirectory(APP_DIR); + } + } + + private static void loadProfile(Consumer msgConsumer) throws Exception { + msgConsumer.accept("Loading the most recent profile."); + Profile.loadLast(); } } \ No newline at end of file diff --git a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java index ff083d0..35923da 100644 --- a/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/AccountsViewController.java @@ -1,7 +1,7 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; -import com.andrewlalis.perfin.control.component.AccountTile; +import com.andrewlalis.perfin.view.component.AccountTile; import com.andrewlalis.perfin.data.CurrencyUtil; import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.pagination.Sort; diff --git a/src/main/java/com/andrewlalis/perfin/control/StartupSplashScreenController.java b/src/main/java/com/andrewlalis/perfin/control/StartupSplashScreenController.java deleted file mode 100644 index 06703c5..0000000 --- a/src/main/java/com/andrewlalis/perfin/control/StartupSplashScreenController.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.andrewlalis.perfin.control; - -import com.andrewlalis.perfin.model.Profile; -import com.andrewlalis.perfin.view.SplashScreenStage; -import javafx.application.Platform; -import javafx.fxml.FXML; -import javafx.scene.control.TextArea; -import javafx.scene.layout.BorderPane; - -import java.io.IOException; -import java.nio.file.Files; - -import static com.andrewlalis.perfin.PerfinApp.APP_DIR; - -/** - * A controller for the application's splash screen that shows initially on - * startup. While the splash screen is shown, we do any complicated loading - * tasks so that the application starts properly, and give the user periodic - * updates as we go. - */ -public class StartupSplashScreenController { - @FXML - public BorderPane sceneRoot; - @FXML - public TextArea content; - - @FXML - public void initialize() { - Thread.ofVirtual().start(() -> { - try { - printlnLater("Initializing application files..."); - if (!initAppDir()) { - Thread.sleep(3000); - Platform.runLater(() -> getSplashStage().setError()); - return; - } - - printlnLater("Loading the last profile..."); - try { - Profile.loadLast(); - } catch (Exception e) { - printlnLater("Failed to load profile: " + e.getMessage()); - Thread.sleep(3000); - Platform.runLater(() -> getSplashStage().setError()); - return; - } - - - printlnLater("Perfin initialized. Starting the app now."); - Thread.sleep(500); - - Platform.runLater(() -> getSplashStage().setDone()); - } catch (Exception e) { - e.printStackTrace(System.err); - printlnLater("An error occurred while starting: " + e.getMessage() + "\nThe application will now exit."); - Platform.runLater(() -> getSplashStage().setError()); - } - }); - } - - private void println(String text) { - content.appendText(text + "\n"); - } - - private void printlnLater(String text) { - Platform.runLater(() -> println(text)); - } - - private SplashScreenStage getSplashStage() { - return (SplashScreenStage) sceneRoot.getScene().getWindow(); - } - - private boolean initAppDir() { - if (Files.notExists(APP_DIR)) { - printlnLater(APP_DIR + " doesn't exist yet. Creating it now."); - try { - Files.createDirectory(APP_DIR); - } catch (IOException e) { - printlnLater("Could not create directory " + APP_DIR + "; " + e.getMessage()); - return false; - } - } else if (Files.exists(APP_DIR) && Files.isRegularFile(APP_DIR)) { - printlnLater(APP_DIR + " is a file, when it should be a directory. Deleting it and creating new directory."); - try { - Files.delete(APP_DIR); - Files.createDirectory(APP_DIR); - } catch (IOException e) { - printlnLater("Could not delete file and create directory " + APP_DIR + "; " + e.getMessage()); - return false; - } - } - return true; - } -} diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java index 22e8008..658c3b6 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionViewController.java @@ -1,6 +1,6 @@ package com.andrewlalis.perfin.control; -import com.andrewlalis.perfin.control.component.AttachmentPreview; +import com.andrewlalis.perfin.view.component.AttachmentPreview; import com.andrewlalis.perfin.data.CurrencyUtil; import com.andrewlalis.perfin.data.DateUtil; import com.andrewlalis.perfin.model.CreditAndDebitAccounts; @@ -15,6 +15,8 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.skin.ScrollPaneSkin; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.text.TextFlow; @@ -70,6 +72,10 @@ public class TransactionViewController { Platform.runLater(() -> attachmentsList.setAll(attachments)); }); }); + attachmentsHBox.setMinHeight(AttachmentPreview.HEIGHT); + attachmentsHBox.setPrefHeight(AttachmentPreview.HEIGHT); + ((ScrollPane) attachmentsHBox.getParent().getParent().getParent()).minHeightProperty().bind(attachmentsHBox.heightProperty().map(n -> n.doubleValue() + 2)); + ((ScrollPane) attachmentsHBox.getParent().getParent().getParent()).prefHeightProperty().bind(attachmentsHBox.heightProperty().map(n -> n.doubleValue() + 2)); BindingUtil.mapContent(attachmentsHBox.getChildren(), attachmentsList, AttachmentPreview::new); } diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java index 03bff07..d5c864c 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java @@ -3,8 +3,8 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.perfin.Pair; import com.andrewlalis.perfin.SceneUtil; -import com.andrewlalis.perfin.control.component.DataSourcePaginationControls; -import com.andrewlalis.perfin.control.component.TransactionTile; +import com.andrewlalis.perfin.view.component.DataSourcePaginationControls; +import com.andrewlalis.perfin.view.component.TransactionTile; import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.pagination.Sort; diff --git a/src/main/java/com/andrewlalis/perfin/view/SplashScreenStage.java b/src/main/java/com/andrewlalis/perfin/view/SplashScreenStage.java deleted file mode 100644 index 159b0c7..0000000 --- a/src/main/java/com/andrewlalis/perfin/view/SplashScreenStage.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.andrewlalis.perfin.view; - -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.scene.Scene; -import javafx.stage.Stage; -import javafx.stage.StageStyle; - -public class SplashScreenStage extends Stage { - public enum State { - LOADING, - DONE, - ERROR - } - - private final SimpleObjectProperty stateProperty = new SimpleObjectProperty<>(State.LOADING); - - public SplashScreenStage(String title, Scene scene) { - setTitle(title); - setResizable(false); - initStyle(StageStyle.UNDECORATED); - setScene(scene); - } - - public void setDone() { - stateProperty.set(State.DONE); - close(); - } - - public void setError() { - stateProperty.set(State.ERROR); - close(); - } - - public ObservableValue stateProperty() { - return this.stateProperty; - } -} diff --git a/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java new file mode 100644 index 0000000..ee9a56c --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/view/StartupSplashScreen.java @@ -0,0 +1,82 @@ +package com.andrewlalis.perfin.view; + +import com.andrewlalis.perfin.data.ThrowableConsumer; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.control.TextArea; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +import java.util.List; +import java.util.function.Consumer; + +/** + * A splash screen that is shown as the application starts up, and does some + * tasks before the main application can start. + */ +public class StartupSplashScreen extends Stage implements Consumer { + private final List>> tasks; + private boolean startupSuccessful = false; + + private final TextArea textArea = new TextArea(); + + public StartupSplashScreen(List>> tasks) { + this.tasks = tasks; + setTitle("Starting Perfin..."); + setResizable(false); + initStyle(StageStyle.UNDECORATED); + + setScene(buildScene()); + setOnShowing(event -> runTasks()); + } + + public boolean isStartupSuccessful() { + return startupSuccessful; + } + + @Override + public void accept(String message) { + Platform.runLater(() -> textArea.appendText(message + "\n")); + } + + private Scene buildScene() { + BorderPane root = new BorderPane(textArea); + root.setId("sceneRoot"); + root.setPrefWidth(400.0); + root.setPrefHeight(200.0); + + textArea.setId("content"); + textArea.setWrapText(true); + textArea.setEditable(false); + textArea.setFocusTraversable(false); + + Scene scene = new Scene(root, 400.0, 200.0); + scene.getStylesheets().add(StartupSplashScreen.class.getResource("/style/startup-splash-screen.css").toExternalForm()); + return scene; + } + + private void runTasks() { + Thread.ofVirtual().start(() -> { + for (var task : tasks) { + try { + task.accept(this); + Thread.sleep(100); + } catch (Exception e) { + accept("Startup failed: " + e.getMessage()); + e.printStackTrace(System.err); + Platform.runLater(this::close); + return; + } + } + accept("Startup successful!"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + startupSuccessful = true; + Platform.runLater(this::close); + }); + } +} diff --git a/src/main/java/com/andrewlalis/perfin/control/component/AccountTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java similarity index 98% rename from src/main/java/com/andrewlalis/perfin/control/component/AccountTile.java rename to src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java index 02d67da..b43245c 100644 --- a/src/main/java/com/andrewlalis/perfin/control/component/AccountTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java @@ -1,4 +1,4 @@ -package com.andrewlalis.perfin.control.component; +package com.andrewlalis.perfin.view.component; import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.AccountType; diff --git a/src/main/java/com/andrewlalis/perfin/control/component/AttachmentPreview.java b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java similarity index 64% rename from src/main/java/com/andrewlalis/perfin/control/component/AttachmentPreview.java rename to src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java index 824d4d6..3e97082 100644 --- a/src/main/java/com/andrewlalis/perfin/control/component/AttachmentPreview.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AttachmentPreview.java @@ -1,4 +1,4 @@ -package com.andrewlalis.perfin.control.component; +package com.andrewlalis.perfin.view.component; import com.andrewlalis.perfin.model.TransactionAttachment; import javafx.scene.control.Label; @@ -18,20 +18,27 @@ import java.util.Set; * like its name, type, and a preview image if possible. */ public class AttachmentPreview extends BorderPane { + public static final double IMAGE_SIZE = 64.0; + public static final double LABEL_SIZE = 18.0; + public static final double HEIGHT = IMAGE_SIZE + LABEL_SIZE; + public AttachmentPreview(TransactionAttachment attachment) { Label nameLabel = new Label(attachment.getFilename()); - Label typeLabel = new Label(attachment.getContentType()); - typeLabel.setStyle("-fx-font-size: x-small;"); - setBottom(new VBox(nameLabel, typeLabel)); + nameLabel.setStyle("-fx-font-size: small;"); + VBox nameContainer = new VBox(nameLabel); + nameContainer.setPrefHeight(LABEL_SIZE); + nameContainer.setMaxHeight(LABEL_SIZE); + nameContainer.setMinHeight(LABEL_SIZE); + setBottom(nameContainer); - Rectangle placeholder = new Rectangle(64.0, 64.0); + Rectangle placeholder = new Rectangle(IMAGE_SIZE, IMAGE_SIZE); placeholder.setFill(Color.WHITE); setCenter(placeholder); Set imageTypes = Set.of("image/png", "image/jpeg", "image/gif", "image/bmp"); if (imageTypes.contains(attachment.getContentType())) { try (var in = Files.newInputStream(attachment.getPath())) { - Image img = new Image(in, 64.0, 64.0, true, true); + Image img = new Image(in, IMAGE_SIZE, IMAGE_SIZE, true, true); setCenter(new ImageView(img)); } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/com/andrewlalis/perfin/control/component/DataSourcePaginationControls.java b/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java similarity index 98% rename from src/main/java/com/andrewlalis/perfin/control/component/DataSourcePaginationControls.java rename to src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java index dc07992..3911533 100644 --- a/src/main/java/com/andrewlalis/perfin/control/component/DataSourcePaginationControls.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/DataSourcePaginationControls.java @@ -1,4 +1,4 @@ -package com.andrewlalis.perfin.control.component; +package com.andrewlalis.perfin.view.component; import com.andrewlalis.perfin.data.pagination.Page; import com.andrewlalis.perfin.data.pagination.PageRequest; diff --git a/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java similarity index 98% rename from src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java rename to src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java index 5d4c874..24a3cea 100644 --- a/src/main/java/com/andrewlalis/perfin/control/component/TransactionTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/TransactionTile.java @@ -1,4 +1,4 @@ -package com.andrewlalis.perfin.control.component; +package com.andrewlalis.perfin.view.component; import com.andrewlalis.perfin.data.CurrencyUtil; import com.andrewlalis.perfin.data.DateUtil; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 00a482a..f70e4dc 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -12,6 +12,8 @@ module com.andrewlalis.perfin { exports com.andrewlalis.perfin to javafx.graphics; exports com.andrewlalis.perfin.view to javafx.graphics; exports com.andrewlalis.perfin.model to javafx.graphics; + opens com.andrewlalis.perfin.control to javafx.fxml; - opens com.andrewlalis.perfin.control.component to javafx.fxml; + opens com.andrewlalis.perfin.view to javafx.fxml; + opens com.andrewlalis.perfin.view.component to javafx.fxml; } \ No newline at end of file diff --git a/src/main/resources/startup-splash-screen.fxml b/src/main/resources/startup-splash-screen.fxml deleted file mode 100644 index ff47cdf..0000000 --- a/src/main/resources/startup-splash-screen.fxml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - -
-