Added an improved splash screen, and improved formatting of attachments list.

This commit is contained in:
Andrew Lalis 2023-12-29 19:47:31 -05:00
parent 0eb2edfc8d
commit 7f85591567
16 changed files with 221 additions and 184 deletions

41
pom.xml
View File

@ -13,6 +13,7 @@
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>21.0.1</javafx.version>
<project.main-class>com.andrewlalis.perfin.PerfinApp</project.main-class>
</properties>
<dependencies>
@ -60,6 +61,46 @@
<mainClass>com.andrewlalis.perfin.PerfinApp</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.andrewlalis.perfin.PerfinApp</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

19
run-jar.sh Executable file
View File

@ -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

View File

@ -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) {
private void initMainScreen(Stage stage, Consumer<String> 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() {
private static void defineRoutes(Consumer<String> 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");
mapResourceRoute("transaction", "/transaction-view.fxml");
});
}
private static void initAppDir(Consumer<String> 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<String> msgConsumer) throws Exception {
msgConsumer.accept("Loading the most recent profile.");
Profile.loadLast();
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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<State> 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<State> stateProperty() {
return this.stateProperty;
}
}

View File

@ -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<String> {
private final List<ThrowableConsumer<Consumer<String>>> tasks;
private boolean startupSuccessful = false;
private final TextArea textArea = new TextArea();
public StartupSplashScreen(List<ThrowableConsumer<Consumer<String>>> 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);
});
}
}

View File

@ -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;

View File

@ -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<String> 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();

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane
fx:id="sceneRoot"
prefHeight="200" prefWidth="400"
xmlns="http://javafx.com/javafx/17.0.2-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.andrewlalis.perfin.control.StartupSplashScreenController"
stylesheets="@style/startup-splash-screen.css"
>
<center>
<TextArea fx:id="content" wrapText="true" editable="false" focusTraversable="false"/>
</center>
</BorderPane>

View File

@ -41,7 +41,6 @@
<HBox fx:id="attachmentsHBox" styleClass="std-padding,std-spacing"/>
</ScrollPane>
</VBox>
<Separator/>
<FlowPane styleClass="std-padding, std-spacing">
<Button text="Delete" onAction="#deleteTransaction"/>
</FlowPane>