Added an improved splash screen, and improved formatting of attachments list.
This commit is contained in:
parent
0eb2edfc8d
commit
7f85591567
41
pom.xml
41
pom.xml
|
@ -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>
|
|
@ -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
|
|
@ -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<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() {
|
||||
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<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");
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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();
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue