Added profiles selector and logic for changing the selected profile.
This commit is contained in:
parent
7d7f80676a
commit
755dc87aec
|
@ -35,7 +35,7 @@ public class PerfinApp extends Application {
|
|||
PerfinApp::defineRoutes,
|
||||
PerfinApp::initAppDir,
|
||||
c -> initMainScreen(stage, c),
|
||||
PerfinApp::loadProfile
|
||||
PerfinApp::loadLastUsedProfile
|
||||
));
|
||||
splashScreen.showAndWait();
|
||||
if (splashScreen.isStartupSuccessful()) {
|
||||
|
@ -43,7 +43,7 @@ public class PerfinApp extends Application {
|
|||
}
|
||||
}
|
||||
|
||||
private void initMainScreen(Stage stage, Consumer<String> msgConsumer) throws Exception {
|
||||
private void initMainScreen(Stage stage, Consumer<String> msgConsumer) {
|
||||
msgConsumer.accept("Initializing main screen.");
|
||||
Platform.runLater(() -> {
|
||||
stage.hide();
|
||||
|
@ -57,7 +57,7 @@ public class PerfinApp extends Application {
|
|||
router.map(route, PerfinApp.class.getResource(resource));
|
||||
}
|
||||
|
||||
private static void defineRoutes(Consumer<String> msgConsumer) throws Exception {
|
||||
private static void defineRoutes(Consumer<String> msgConsumer) {
|
||||
msgConsumer.accept("Initializing application views.");
|
||||
Platform.runLater(() -> {
|
||||
mapResourceRoute("accounts", "/accounts-view.fxml");
|
||||
|
@ -80,7 +80,7 @@ public class PerfinApp extends Application {
|
|||
}
|
||||
}
|
||||
|
||||
private static void loadProfile(Consumer<String> msgConsumer) throws Exception {
|
||||
private static void loadLastUsedProfile(Consumer<String> msgConsumer) throws Exception {
|
||||
msgConsumer.accept("Loading the most recent profile.");
|
||||
Profile.loadLast();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ 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 javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
|
@ -10,13 +11,10 @@ import javafx.scene.layout.HBox;
|
|||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
||||
public class MainViewController {
|
||||
@FXML
|
||||
public BorderPane mainContainer;
|
||||
@FXML
|
||||
public HBox breadcrumbHBox;
|
||||
@FXML public BorderPane mainContainer;
|
||||
@FXML public HBox breadcrumbHBox;
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
@FXML public void initialize() {
|
||||
AnchorPaneRouterView routerView = (AnchorPaneRouterView) router.getView();
|
||||
mainContainer.setCenter(routerView.getAnchorPane());
|
||||
|
||||
|
@ -36,25 +34,25 @@ public class MainViewController {
|
|||
router.navigate("accounts");
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void goBack() {
|
||||
@FXML public void goBack() {
|
||||
router.navigateBack();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void goForward() {
|
||||
@FXML public void goForward() {
|
||||
router.navigateForward();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void goToAccounts() {
|
||||
@FXML public void goToAccounts() {
|
||||
router.getHistory().clear();
|
||||
router.navigate("accounts");
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void goToTransactions() {
|
||||
@FXML public void goToTransactions() {
|
||||
router.getHistory().clear();
|
||||
router.navigate("transactions");
|
||||
}
|
||||
|
||||
@FXML public void viewProfiles() {
|
||||
ProfilesStage.open(mainContainer.getScene().getWindow());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,4 +11,17 @@ public class Popups {
|
|||
var result = alert.showAndWait();
|
||||
return result.isPresent() && result.get() == ButtonType.OK;
|
||||
}
|
||||
|
||||
public static void message(String text) {
|
||||
Alert alert = new Alert(Alert.AlertType.NONE, text);
|
||||
alert.initModality(Modality.APPLICATION_MODAL);
|
||||
alert.getButtonTypes().setAll(ButtonType.OK);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
public static void error(String text) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING, text);
|
||||
alert.initModality(Modality.APPLICATION_MODAL);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package com.andrewlalis.perfin.control;
|
||||
|
||||
import com.andrewlalis.perfin.data.FileUtil;
|
||||
import com.andrewlalis.perfin.model.Profile;
|
||||
import com.andrewlalis.perfin.view.ProfilesStage;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
||||
public class ProfilesViewController {
|
||||
@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());
|
||||
addProfileButton.disableProperty().bind(newProfileNameValid.not());
|
||||
|
||||
refreshAvailableProfiles();
|
||||
}
|
||||
|
||||
@FXML public void addProfile() {
|
||||
String name = newProfileNameField.getText();
|
||||
boolean valid = Profile.validateName(name);
|
||||
if (valid && !Profile.getAvailableProfiles().contains(name)) {
|
||||
boolean confirm = Popups.confirm("Are you sure you want to add a new profile named \"" + name + "\"?");
|
||||
if (confirm) {
|
||||
if (openProfile(name, false)) {
|
||||
Popups.message("Created new profile \"" + name + "\" and loaded it.");
|
||||
}
|
||||
newProfileNameField.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshAvailableProfiles() {
|
||||
List<String> profileNames = Profile.getAvailableProfiles();
|
||||
String currentProfile = Profile.getCurrent() == null ? null : Profile.getCurrent().getName();
|
||||
List<Node> nodes = new ArrayList<>(profileNames.size());
|
||||
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;
|
||||
""");
|
||||
|
||||
Text nameTextElement = new Text(profileName);
|
||||
nameTextElement.setStyle("-fx-font-size: large;");
|
||||
TextFlow nameLabel = new TextFlow(nameTextElement);
|
||||
if (isCurrent) {
|
||||
nameTextElement.setStyle("-fx-font-size: large; -fx-font-weight: bold;");
|
||||
Text currentProfileIndicator = new Text(" Currently Selected Profile");
|
||||
currentProfileIndicator.setStyle("""
|
||||
-fx-font-size: small;
|
||||
-fx-fill: grey;
|
||||
""");
|
||||
nameLabel.getChildren().add(currentProfileIndicator);
|
||||
}
|
||||
AnchorPane.setLeftAnchor(nameLabel, 0.0);
|
||||
AnchorPane.setTopAnchor(nameLabel, 0.0);
|
||||
AnchorPane.setBottomAnchor(nameLabel, 0.0);
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
AnchorPane.setRightAnchor(buttonBox, 0.0);
|
||||
AnchorPane.setTopAnchor(buttonBox, 0.0);
|
||||
AnchorPane.setBottomAnchor(buttonBox, 0.0);
|
||||
buttonBox.getStyleClass().addAll("std-spacing");
|
||||
Button openButton = new Button("Open");
|
||||
openButton.setOnAction(event -> openProfile(profileName, false));
|
||||
openButton.setDisable(isCurrent);
|
||||
buttonBox.getChildren().add(openButton);
|
||||
Button deleteButton = new Button("Delete");
|
||||
deleteButton.setOnAction(event -> deleteProfile(profileName));
|
||||
buttonBox.getChildren().add(deleteButton);
|
||||
|
||||
profilePane.getChildren().setAll(nameLabel, buttonBox);
|
||||
nodes.add(profilePane);
|
||||
}
|
||||
profilesVBox.getChildren().setAll(nodes);
|
||||
}
|
||||
|
||||
private boolean openProfile(String name, boolean showPopup) {
|
||||
System.out.println("Opening profile: " + name);
|
||||
try {
|
||||
Profile.load(name);
|
||||
ProfilesStage.closeView();
|
||||
router.getHistory().clear();
|
||||
router.navigate("accounts");
|
||||
if (showPopup) Popups.message("The profile \"" + name + "\" has been loaded.");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.err);
|
||||
Popups.error("Failed to load profile: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteProfile(String name) {
|
||||
boolean confirmA = Popups.confirm("Are you sure you want to delete the profile \"" + name + "\"? This will permanently delete ALL accounts, transactions, files, and other data for this profile, and it cannot be recovered.");
|
||||
if (confirmA) {
|
||||
boolean confirmB = Popups.confirm("Press \"OK\" to confirm that you really want to delete the profile \"" + name + "\". There's no going back.");
|
||||
if (confirmB) {
|
||||
try {
|
||||
FileUtil.deleteDirRecursive(Profile.getDir(name));
|
||||
// Reset the app's "last profile" to the default if it was the deleted profile.
|
||||
if (Profile.getLastProfile().equals(name)) {
|
||||
Profile.saveLastProfile("default");
|
||||
}
|
||||
// If the current profile was deleted, switch to the default.
|
||||
if (Profile.getCurrent() != null && Profile.getCurrent().getName().equals(name)) {
|
||||
openProfile("default", true);
|
||||
}
|
||||
refreshAvailableProfiles();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
package com.andrewlalis.perfin.data;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -22,4 +28,20 @@ public class FileUtil {
|
|||
MIMETYPES.put(".bmp", "image/bmp");
|
||||
MIMETYPES.put(".tiff", "image/tiff");
|
||||
}
|
||||
|
||||
public static void deleteDirRecursive(Path startDir) throws IOException {
|
||||
Files.walkFileTree(startDir, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
|
@ -86,6 +83,16 @@ public class Profile {
|
|||
}
|
||||
}
|
||||
|
||||
public static List<String> getAvailableProfiles() {
|
||||
try (var files = Files.list(PerfinApp.APP_DIR)) {
|
||||
return files.filter(Files::isDirectory)
|
||||
.map(path -> path.getFileName().toString())
|
||||
.sorted().toList();
|
||||
} catch (IOException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLastProfile() {
|
||||
Path lastProfileFile = PerfinApp.APP_DIR.resolve("last-profile.txt");
|
||||
if (Files.exists(lastProfileFile)) {
|
||||
|
@ -127,7 +134,6 @@ public class Profile {
|
|||
for (var c : profileLoadListeners) {
|
||||
c.accept(current);
|
||||
}
|
||||
profileLoadListeners.clear();
|
||||
}
|
||||
|
||||
private static void initProfileDir(String name) throws IOException {
|
||||
|
@ -181,6 +187,13 @@ public class Profile {
|
|||
}
|
||||
|
||||
public static boolean validateName(String name) {
|
||||
return name.matches("\\w+");
|
||||
return name != null &&
|
||||
name.matches("\\w+") &&
|
||||
name.toLowerCase().equals(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package com.andrewlalis.perfin.view;
|
||||
|
||||
import com.andrewlalis.perfin.SceneUtil;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* A stage that shows a popup for interacting with Perfin's collection of
|
||||
* profiles.
|
||||
*/
|
||||
public class ProfilesStage extends Stage {
|
||||
private static ProfilesStage instance;
|
||||
|
||||
public ProfilesStage() {
|
||||
setTitle("Profiles");
|
||||
setAlwaysOnTop(false);
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
setScene(SceneUtil.load("/profiles-view.fxml"));
|
||||
}
|
||||
|
||||
public static void open(Window owner) {
|
||||
if (instance == null) {
|
||||
instance = new ProfilesStage();
|
||||
instance.initOwner(owner);
|
||||
instance.show();
|
||||
instance.setOnCloseRequest(event -> instance = null);
|
||||
} else {
|
||||
instance.requestFocus();
|
||||
instance.toFront();
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeView() {
|
||||
if (instance != null) {
|
||||
instance.close();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
<Button text="Forward" onAction="#goForward"/>
|
||||
<Button text="Accounts" onAction="#goToAccounts"/>
|
||||
<Button text="Transactions" onAction="#goToTransactions"/>
|
||||
<Button text="Profiles" onAction="#viewProfiles"/>
|
||||
</HBox>
|
||||
<HBox fx:id="breadcrumbHBox" styleClass="std-spacing,small-text"/>
|
||||
</VBox>
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ScrollPane?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<BorderPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="com.andrewlalis.perfin.control.ProfilesViewController"
|
||||
stylesheets="@style/base.css"
|
||||
prefWidth="500"
|
||||
prefHeight="400"
|
||||
>
|
||||
<top>
|
||||
<VBox styleClass="std-padding,std-spacing">
|
||||
<TextFlow>
|
||||
<Text text="In Perfin, all your accounts, transactions, files, and other financial data are stored in a single "/>
|
||||
<Text text="profile" styleClass="bold-text"/>
|
||||
<Text text=". By default, Perfin uses the "/>
|
||||
<Text text="default" style="-fx-font-style: italic;"/>
|
||||
<Text text=" profile, and this should be sufficient for most users, but you can also add new profiles if you'd like to track some finances separately."/>
|
||||
</TextFlow>
|
||||
</VBox>
|
||||
</top>
|
||||
<center>
|
||||
<ScrollPane fitToWidth="true" fitToHeight="true">
|
||||
<VBox fx:id="profilesVBox" styleClass="std-padding,spacing-extra"/>
|
||||
</ScrollPane>
|
||||
</center>
|
||||
<bottom>
|
||||
<BorderPane>
|
||||
<left>
|
||||
<VBox styleClass="std-padding">
|
||||
<Label text="Add New Profile"/>
|
||||
</VBox>
|
||||
</left>
|
||||
<center>
|
||||
<VBox styleClass="std-padding">
|
||||
<TextField fx:id="newProfileNameField" style="-fx-min-width: 50px; -fx-pref-width: 50px;"/>
|
||||
<Text
|
||||
fx:id="newProfileNameErrorLabel"
|
||||
styleClass="error-text"
|
||||
style="-fx-fill: red;"
|
||||
text="Invalid profile name. Profile names must only contain lowercase text."
|
||||
/>
|
||||
</VBox>
|
||||
</center>
|
||||
<right>
|
||||
<VBox styleClass="std-padding">
|
||||
<Button text="Add" onAction="#addProfile" fx:id="addProfileButton"/>
|
||||
</VBox>
|
||||
</right>
|
||||
</BorderPane>
|
||||
</bottom>
|
||||
</BorderPane>
|
Loading…
Reference in New Issue