diff --git a/launcher/pom.xml b/launcher/pom.xml
index 38b3094..00d1d5a 100644
--- a/launcher/pom.xml
+++ b/launcher/pom.xml
@@ -26,6 +26,12 @@
javafx-fxml
${javafx.version}
+
+
+ com.google.code.gson
+ gson
+ 2.9.1
+
diff --git a/launcher/src/main/java/module-info.java b/launcher/src/main/java/module-info.java
index 5edf66c..071f7a0 100644
--- a/launcher/src/main/java/module-info.java
+++ b/launcher/src/main/java/module-info.java
@@ -4,6 +4,10 @@ module aos2_launcher {
requires javafx.graphics;
requires javafx.fxml;
+ requires java.net.http;
+ requires com.google.gson;
+
exports nl.andrewl.aos2_launcher to javafx.graphics;
opens nl.andrewl.aos2_launcher to javafx.fxml;
+ opens nl.andrewl.aos2_launcher.view to javafx.fxml;
}
\ No newline at end of file
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/EditProfileController.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/EditProfileController.java
new file mode 100644
index 0000000..7197aa3
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/EditProfileController.java
@@ -0,0 +1,4 @@
+package nl.andrewl.aos2_launcher;
+
+public class EditProfileController {
+}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java
index bce8874..78330ff 100644
--- a/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java
@@ -7,22 +7,41 @@ import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
/**
* The main starting point for the launcher app.
*/
public class Launcher extends Application {
+ public static final Path BASE_DIR = Path.of(System.getProperty("user.home"), ".ace-of-shades");
+ public static final Path VERSIONS_DIR = BASE_DIR.resolve("versions");
+ public static final Path PROFILES_FILE = BASE_DIR.resolve("profiles.json");
+
@Override
public void start(Stage stage) throws IOException {
+ if (!Files.exists(BASE_DIR)) {
+ Files.createDirectory(BASE_DIR);
+ }
+ if (!Files.exists(VERSIONS_DIR)) {
+ Files.createDirectory(VERSIONS_DIR);
+ }
FXMLLoader loader = new FXMLLoader(Launcher.class.getResource("/main_view.fxml"));
Parent rootNode = loader.load();
Scene scene = new Scene(rootNode);
- scene.getStylesheets().add(Launcher.class.getResource("/styles.css").toExternalForm());
+ addStylesheet(scene, "/font/fonts.css");
+ addStylesheet(scene, "/styles.css");
stage.setScene(scene);
stage.setTitle("Ace of Shades 2 - Launcher");
stage.show();
}
+ private void addStylesheet(Scene scene, String resource) throws IOException {
+ var url = Launcher.class.getResource(resource);
+ if (url == null) throw new IOException("Could not load resource at " + resource);
+ scene.getStylesheets().add(url.toExternalForm());
+ }
+
public static void main(String[] args) {
launch(args);
}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java
index 92e8338..beea6ae 100644
--- a/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java
@@ -1,16 +1,216 @@
package nl.andrewl.aos2_launcher;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
import javafx.application.Platform;
-import javafx.event.ActionEvent;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.css.PseudoClass;
import javafx.fxml.FXML;
-import javafx.scene.layout.TilePane;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.VBox;
+import nl.andrewl.aos2_launcher.model.Profile;
+import nl.andrewl.aos2_launcher.model.Server;
+import nl.andrewl.aos2_launcher.view.BindingUtil;
+import nl.andrewl.aos2_launcher.view.EditProfileDialog;
+import nl.andrewl.aos2_launcher.view.ProfileView;
+import nl.andrewl.aos2_launcher.view.ServerView;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
public class MainViewController {
@FXML
- public TilePane profilesTilePane;
+ public Button playButton;
+ @FXML
+ public Button editProfileButton;
+ @FXML
+ public Button removeProfileButton;
+ @FXML
+ public VBox profilesVBox;
+ @FXML
+ public VBox serversVBox;
+ @FXML
+ public Label selectedProfileLabel;
+ @FXML
+ public Label selectedServerLabel;
+ private final ObservableList profiles = FXCollections.observableArrayList();
+ private final ObservableList servers = FXCollections.observableArrayList();
+ private final ObjectProperty selectedServer = new SimpleObjectProperty<>(null);
+ private final ObjectProperty selectedProfile = new SimpleObjectProperty<>(null);
- public void onExit(ActionEvent actionEvent) {
- Platform.exit();
+ private final ServersFetcher serversFetcher = new ServersFetcher();
+
+ @FXML
+ public void initialize() {
+ BindingUtil.mapContent(serversVBox.getChildren(), servers, ServerView::new);
+ BindingUtil.mapContent(profilesVBox.getChildren(), profiles, ProfileView::new);
+ selectedProfile.addListener((observable, oldValue, newValue) -> {
+ if (newValue == null) {
+ selectedProfileLabel.setText("None");
+ } else {
+ selectedProfileLabel.setText(newValue.getName());
+ }
+ });
+ selectedServer.addListener((observable, oldValue, newValue) -> {
+ if (newValue == null) {
+ selectedServerLabel.setText("None");
+ } else {
+ selectedServerLabel.setText(newValue.getName());
+ }
+ });
+ BooleanBinding playBind = selectedProfile.isNull().or(selectedServer.isNull());
+ playButton.disableProperty().bind(playBind);
+ editProfileButton.disableProperty().bind(selectedProfile.isNull());
+ removeProfileButton.disableProperty().bind(selectedProfile.isNull());
+
+ profilesVBox.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
+ Node target = (Node) event.getTarget();
+ while (target != null) {
+ if (target instanceof ProfileView view) {
+ if (view.getProfile().equals(selectedProfile.get()) && event.isControlDown()) {
+ selectProfile(null);
+ } else if (!event.isControlDown() && event.getClickCount() == 2) {
+ selectProfile(view);
+ editProfile();
+ } else {
+ selectProfile(view);
+ }
+ return;
+ }
+ target = target.getParent();
+ }
+ selectProfile(null);
+ });
+ serversVBox.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
+ Node target = (Node) event.getTarget();
+ while (target != null) {
+ if (target instanceof ServerView view) {
+ if (view.getServer().equals(selectedServer.get()) && event.isControlDown()) {
+ selectServer(null);
+ } else {
+ selectServer(view);
+ }
+ return;
+ }
+ target = target.getParent();
+ }
+ selectServer(null);
+ });
+ loadProfiles();
+ refreshServers();
+ }
+
+ @FXML
+ public void refreshServers() {
+ serversFetcher.fetchServers()
+ .exceptionally(throwable -> {
+ throwable.printStackTrace();
+ return new ArrayList<>();
+ })
+ .thenAccept(newServers -> {
+ Platform.runLater(() -> {
+ this.servers.clear();
+ this.servers.addAll(newServers);
+ });
+ });
+ }
+
+ @FXML
+ public void addProfile() {
+ EditProfileDialog dialog = new EditProfileDialog(profilesVBox.getScene().getWindow());
+ dialog.showAndWait().ifPresent(profiles::add);
+ saveProfiles();
+ }
+
+ @FXML
+ public void editProfile() {
+ EditProfileDialog dialog = new EditProfileDialog(profilesVBox.getScene().getWindow(), selectedProfile.get());
+ dialog.showAndWait();
+ saveProfiles();
+ }
+
+ @FXML
+ public void removeProfile() {
+ if (selectedProfile.getValue() != null) {
+ profiles.remove(selectedProfile.getValue());
+ saveProfiles();
+ }
+ }
+
+ @FXML
+ public void play() {
+
+ }
+
+ private void selectProfile(ProfileView view) {
+ Profile profile = view == null ? null : view.getProfile();
+ selectedProfile.set(profile);
+ PseudoClass selectedClass = PseudoClass.getPseudoClass("selected");
+ for (var node : profilesVBox.getChildren()) {
+ node.pseudoClassStateChanged(selectedClass, false);
+ }
+ if (view != null) {
+ view.pseudoClassStateChanged(selectedClass, true);
+ }
+ }
+
+ private void selectServer(ServerView view) {
+ Server server = view == null ? null : view.getServer();
+ selectedServer.set(server);
+ PseudoClass selectedClass = PseudoClass.getPseudoClass("selected");
+ for (var node : serversVBox.getChildren()) {
+ node.pseudoClassStateChanged(selectedClass, false);
+ }
+ if (view != null) {
+ view.pseudoClassStateChanged(selectedClass, true);
+ }
+ }
+
+ private void loadProfiles() {
+ if (!Files.exists(Launcher.PROFILES_FILE)) return;
+ try (var reader = Files.newBufferedReader(Launcher.PROFILES_FILE)) {
+ profiles.clear();
+ JsonArray array = new Gson().fromJson(reader, JsonArray.class);
+ for (var element : array) {
+ if (element.isJsonObject()) {
+ JsonObject obj = element.getAsJsonObject();
+ Profile profile = new Profile();
+ profile.setName(obj.get("name").getAsString());
+ profile.setDescription(obj.get("description").getAsString());
+ profile.setClientVersion(obj.get("clientVersion").getAsString());
+ profiles.add(profile);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void saveProfiles() {
+ JsonArray array = new JsonArray(profiles.size());
+ for (var profile : profiles) {
+ JsonObject obj = new JsonObject();
+ obj.addProperty("name", profile.getName());
+ obj.addProperty("description", profile.getDescription());
+ obj.addProperty("clientVersion", profile.getClientVersion());
+ array.add(obj);
+ }
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ try (var writer = Files.newBufferedWriter(Launcher.PROFILES_FILE)) {
+ gson.toJson(array, writer);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/ServersFetcher.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/ServersFetcher.java
new file mode 100644
index 0000000..b32e641
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/ServersFetcher.java
@@ -0,0 +1,59 @@
+package nl.andrewl.aos2_launcher;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import nl.andrewl.aos2_launcher.model.Server;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+class ServersFetcher {
+ private static final String registryURL = "http://localhost:8080";
+
+ private final HttpClient httpClient;
+ private final Gson gson;
+
+ public ServersFetcher() {
+ httpClient = HttpClient.newBuilder().build();
+ gson = new Gson();
+ }
+
+ public CompletableFuture> fetchServers() {
+ HttpRequest req = HttpRequest.newBuilder(URI.create(registryURL + "/servers"))
+ .GET()
+ .timeout(Duration.ofSeconds(3))
+ .header("Accept", "application/json")
+ .build();
+ return httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString())
+ .thenApplyAsync(resp -> {
+ if (resp.statusCode() == 200) {
+ JsonArray serversArray = gson.fromJson(resp.body(), JsonArray.class);
+ List servers = new ArrayList<>(serversArray.size());
+ for (JsonElement serverJson : serversArray) {
+ if (serverJson instanceof JsonObject obj) {
+ servers.add(new Server(
+ obj.get("host").getAsString(),
+ obj.get("port").getAsInt(),
+ obj.get("name").getAsString(),
+ obj.get("description").getAsString(),
+ obj.get("maxPlayers").getAsInt(),
+ obj.get("currentPlayers").getAsInt(),
+ obj.get("lastUpdatedAt").getAsLong()
+ ));
+ }
+ }
+ return servers;
+ } else {
+ throw new RuntimeException("Invalid response: " + resp.statusCode());
+ }
+ });
+ }
+}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java
new file mode 100644
index 0000000..51e65a2
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java
@@ -0,0 +1,52 @@
+package nl.andrewl.aos2_launcher.model;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+
+public class Profile {
+ private final StringProperty name;
+ private final StringProperty description;
+ private final StringProperty clientVersion;
+
+ public Profile() {
+ this.name = new SimpleStringProperty("");
+ this.description = new SimpleStringProperty(null);
+ this.clientVersion = new SimpleStringProperty(null);
+ }
+
+ public String getName() {
+ return name.get();
+ }
+
+ public StringProperty nameProperty() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description.get();
+ }
+
+ public StringProperty descriptionProperty() {
+ return description;
+ }
+
+ public String getClientVersion() {
+ return clientVersion.get();
+ }
+
+ public StringProperty clientVersionProperty() {
+ return clientVersion;
+ }
+
+ public void setName(String name) {
+ this.name.set(name);
+ }
+
+ public void setDescription(String description) {
+ this.description.set(description);
+ }
+
+ public void setClientVersion(String clientVersion) {
+ this.clientVersion.set(clientVersion);
+ }
+}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Server.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Server.java
new file mode 100644
index 0000000..437f7b5
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Server.java
@@ -0,0 +1,84 @@
+package nl.andrewl.aos2_launcher.model;
+
+import javafx.beans.property.*;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+public class Server {
+ private final StringProperty host;
+ private final IntegerProperty port;
+ private final StringProperty name;
+ private final StringProperty description;
+ private final IntegerProperty maxPlayers;
+ private final IntegerProperty currentPlayers;
+ private final ObjectProperty lastUpdatedAt;
+
+ public Server(String host, int port, String name, String description, int maxPlayers, int currentPlayers, long lastUpdatedAt) {
+ this.host = new SimpleStringProperty(host);
+ this.port = new SimpleIntegerProperty(port);
+ this.name = new SimpleStringProperty(name);
+ this.description = new SimpleStringProperty(description);
+ this.maxPlayers = new SimpleIntegerProperty(maxPlayers);
+ this.currentPlayers = new SimpleIntegerProperty(currentPlayers);
+ LocalDateTime ts = Instant.ofEpochMilli(lastUpdatedAt).atZone(ZoneId.systemDefault()).toLocalDateTime();
+ this.lastUpdatedAt = new SimpleObjectProperty<>(ts);
+ }
+
+ public String getHost() {
+ return host.get();
+ }
+
+ public StringProperty hostProperty() {
+ return host;
+ }
+
+ public int getPort() {
+ return port.get();
+ }
+
+ public IntegerProperty portProperty() {
+ return port;
+ }
+
+ public String getName() {
+ return name.get();
+ }
+
+ public StringProperty nameProperty() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description.get();
+ }
+
+ public StringProperty descriptionProperty() {
+ return description;
+ }
+
+ public int getMaxPlayers() {
+ return maxPlayers.get();
+ }
+
+ public IntegerProperty maxPlayersProperty() {
+ return maxPlayers;
+ }
+
+ public int getCurrentPlayers() {
+ return currentPlayers.get();
+ }
+
+ public IntegerProperty currentPlayersProperty() {
+ return currentPlayers;
+ }
+
+ public LocalDateTime getLastUpdatedAt() {
+ return lastUpdatedAt.get();
+ }
+
+ public Property lastUpdatedAtProperty() {
+ return lastUpdatedAt;
+ }
+}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/BindingUtil.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/BindingUtil.java
new file mode 100644
index 0000000..30055c2
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/BindingUtil.java
@@ -0,0 +1,90 @@
+package nl.andrewl.aos2_launcher.view;
+
+import javafx.beans.WeakListener;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.function.Function;
+
+import static java.util.stream.Collectors.toList;
+
+public class BindingUtil {
+ public static void mapContent(ObservableList mapped, ObservableList extends E> source,
+ Function super E, ? extends F> mapper) {
+ map(mapped, source, mapper);
+ }
+
+ private static Object map(ObservableList mapped, ObservableList extends E> source,
+ Function super E, ? extends F> mapper) {
+ final ListContentMapping contentMapping = new ListContentMapping<>(mapped, mapper);
+ mapped.setAll(source.stream().map(mapper).collect(toList()));
+ source.removeListener(contentMapping);
+ source.addListener(contentMapping);
+ return contentMapping;
+ }
+
+ private static class ListContentMapping implements ListChangeListener, WeakListener {
+ private final WeakReference> mappedRef;
+ private final Function super E, ? extends F> mapper;
+
+ public ListContentMapping(List mapped, Function super E, ? extends F> mapper) {
+ this.mappedRef = new WeakReference<>(mapped);
+ this.mapper = mapper;
+ }
+
+ @Override
+ public void onChanged(Change extends E> change) {
+ final List mapped = mappedRef.get();
+ if (mapped == null) {
+ change.getList().removeListener(this);
+ } else {
+ while (change.next()) {
+ if (change.wasPermutated()) {
+ mapped.subList(change.getFrom(), change.getTo()).clear();
+ mapped.addAll(change.getFrom(), change.getList().subList(change.getFrom(), change.getTo())
+ .stream().map(mapper).toList());
+ } else {
+ if (change.wasRemoved()) {
+ mapped.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
+ }
+ if (change.wasAdded()) {
+ mapped.addAll(change.getFrom(), change.getAddedSubList()
+ .stream().map(mapper).toList());
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean wasGarbageCollected() {
+ return mappedRef.get() == null;
+ }
+
+ @Override
+ public int hashCode() {
+ final List list = mappedRef.get();
+ return (list == null) ? 0 : list.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ final List mapped1 = mappedRef.get();
+ if (mapped1 == null) {
+ return false;
+ }
+
+ if (obj instanceof final ListContentMapping, ?> other) {
+ final List> mapped2 = other.mappedRef.get();
+ return mapped1 == mapped2;
+ }
+ return false;
+ }
+ }
+}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/EditProfileDialog.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/EditProfileDialog.java
new file mode 100644
index 0000000..7f9da6d
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/EditProfileDialog.java
@@ -0,0 +1,79 @@
+package nl.andrewl.aos2_launcher.view;
+
+import javafx.application.Platform;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.control.*;
+import javafx.stage.Modality;
+import javafx.stage.Window;
+import nl.andrewl.aos2_launcher.model.Profile;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class EditProfileDialog extends Dialog {
+ @FXML
+ public TextField nameField;
+ @FXML
+ public TextArea descriptionTextArea;
+ @FXML
+ public ChoiceBox clientVersionChoiceBox;
+
+ private final ObjectProperty profile;
+
+ public EditProfileDialog(Window owner, Profile profile) {
+ this.profile = new SimpleObjectProperty<>(profile);
+ try {
+ FXMLLoader loader = new FXMLLoader(EditProfileDialog.class.getResource("/dialog/edit_profile.fxml"));
+ loader.setController(this);
+ Parent parent = loader.load();
+ initOwner(owner);
+ initModality(Modality.APPLICATION_MODAL);
+ setResizable(true);
+ setTitle("Edit Profile");
+
+ BooleanBinding formInvalid = nameField.textProperty().isEmpty()
+ .or(clientVersionChoiceBox.valueProperty().isNull());
+ nameField.setText(profile.getName());
+ descriptionTextArea.setText(profile.getDescription());
+ clientVersionChoiceBox.setItems(FXCollections.observableArrayList("v1.2.0", "v1.3.0", "v1.4.0"));
+ clientVersionChoiceBox.setValue(profile.getClientVersion());
+
+ DialogPane pane = new DialogPane();
+ pane.setContent(parent);
+ ButtonType okButton = new ButtonType("Ok", ButtonBar.ButtonData.OK_DONE);
+ ButtonType cancelButton = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
+ pane.getButtonTypes().add(okButton);
+ pane.getButtonTypes().add(cancelButton);
+ pane.lookupButton(okButton).disableProperty().bind(formInvalid);
+ setDialogPane(pane);
+ setResultConverter(buttonType -> {
+ if (!Objects.equals(ButtonBar.ButtonData.OK_DONE, buttonType.getButtonData())) {
+ return null;
+ }
+ var prof = this.profile.getValue();
+ prof.setName(nameField.getText().trim());
+ String descriptionText = descriptionTextArea.getText().trim();
+ if (descriptionText.isBlank()) {
+ prof.setDescription(null);
+ } else {
+ prof.setDescription(descriptionText);
+ }
+ prof.setClientVersion(clientVersionChoiceBox.getValue());
+ return this.profile.getValue();
+ });
+ setOnShowing(event -> Platform.runLater(() -> nameField.requestFocus()));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public EditProfileDialog(Window owner) {
+ this(owner, new Profile());
+ }
+}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ProfileView.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ProfileView.java
new file mode 100644
index 0000000..7d0a2bd
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ProfileView.java
@@ -0,0 +1,25 @@
+package nl.andrewl.aos2_launcher.view;
+
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+import nl.andrewl.aos2_launcher.model.Profile;
+
+public class ProfileView extends VBox {
+ private final Profile profile;
+
+ public ProfileView(Profile profile) {
+ this.profile = profile;
+ var nameLabel = new Label();
+ nameLabel.textProperty().bind(profile.nameProperty());
+ var descriptionLabel = new Label();
+ descriptionLabel.textProperty().bind(profile.descriptionProperty());
+ var versionLabel = new Label();
+ versionLabel.textProperty().bind(profile.clientVersionProperty());
+ getChildren().addAll(nameLabel, descriptionLabel, versionLabel);
+ getStyleClass().add("list-item");
+ }
+
+ public Profile getProfile() {
+ return this.profile;
+ }
+}
diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ServerView.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ServerView.java
new file mode 100644
index 0000000..5ec8bad
--- /dev/null
+++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ServerView.java
@@ -0,0 +1,33 @@
+package nl.andrewl.aos2_launcher.view;
+
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+import nl.andrewl.aos2_launcher.model.Server;
+
+public class ServerView extends VBox {
+ private final Server server;
+
+ public ServerView(Server server) {
+ this.server = server;
+ var hostLabel = new Label();
+ hostLabel.textProperty().bind(server.hostProperty());
+ var portLabel = new Label();
+ portLabel.setText(Integer.toString(server.getPort()));
+ server.portProperty().addListener((observableValue, x1, x2) -> {
+ portLabel.setText(x2.toString());
+ });
+ var nameLabel = new Label();
+ nameLabel.textProperty().bind(server.nameProperty());
+ var descriptionLabel = new Label();
+ descriptionLabel.textProperty().bind(server.descriptionProperty());
+ var playersLabel = new Label();
+
+ var nodes = getChildren();
+ nodes.addAll(hostLabel, portLabel, nameLabel, descriptionLabel);
+ getStyleClass().add("list-item");
+ }
+
+ public Server getServer() {
+ return server;
+ }
+}
diff --git a/launcher/src/main/resources/dialog/edit_profile.fxml b/launcher/src/main/resources/dialog/edit_profile.fxml
new file mode 100644
index 0000000..7c28f79
--- /dev/null
+++ b/launcher/src/main/resources/dialog/edit_profile.fxml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/src/main/resources/font/JetBrainsMono-Bold.ttf b/launcher/src/main/resources/font/JetBrainsMono-Bold.ttf
new file mode 100644
index 0000000..0a92809
Binary files /dev/null and b/launcher/src/main/resources/font/JetBrainsMono-Bold.ttf differ
diff --git a/launcher/src/main/resources/font/JetBrainsMono-BoldItalic.ttf b/launcher/src/main/resources/font/JetBrainsMono-BoldItalic.ttf
new file mode 100644
index 0000000..d051920
Binary files /dev/null and b/launcher/src/main/resources/font/JetBrainsMono-BoldItalic.ttf differ
diff --git a/launcher/src/main/resources/font/JetBrainsMono-Italic.ttf b/launcher/src/main/resources/font/JetBrainsMono-Italic.ttf
new file mode 100644
index 0000000..e54a46e
Binary files /dev/null and b/launcher/src/main/resources/font/JetBrainsMono-Italic.ttf differ
diff --git a/launcher/src/main/resources/font/JetBrainsMono-Light.ttf b/launcher/src/main/resources/font/JetBrainsMono-Light.ttf
new file mode 100644
index 0000000..dba79a7
Binary files /dev/null and b/launcher/src/main/resources/font/JetBrainsMono-Light.ttf differ
diff --git a/launcher/src/main/resources/font/JetBrainsMono-LightItalic.ttf b/launcher/src/main/resources/font/JetBrainsMono-LightItalic.ttf
new file mode 100644
index 0000000..b5ac216
Binary files /dev/null and b/launcher/src/main/resources/font/JetBrainsMono-LightItalic.ttf differ
diff --git a/launcher/src/main/resources/font/JetBrainsMono-Regular.ttf b/launcher/src/main/resources/font/JetBrainsMono-Regular.ttf
new file mode 100644
index 0000000..8da8aa4
Binary files /dev/null and b/launcher/src/main/resources/font/JetBrainsMono-Regular.ttf differ
diff --git a/launcher/src/main/resources/font/fonts.css b/launcher/src/main/resources/font/fonts.css
new file mode 100644
index 0000000..55beddd
--- /dev/null
+++ b/launcher/src/main/resources/font/fonts.css
@@ -0,0 +1,27 @@
+@font-face {
+ src: url('JetBrainsMono-Regular.ttf');
+}
+
+@font-face {
+ src: url('JetBrainsMono-Bold.ttf');
+}
+
+@font-face {
+ src: url('JetBrainsMono-Light.ttf');
+}
+
+@font-face {
+ src: url('JetBrainsMono-Italic.ttf');
+}
+
+@font-face {
+ src: url('JetBrainsMono-BoldItalic.ttf');
+}
+
+@font-face {
+ src: url('JetBrainsMono-LightItalic.ttf');
+}
+
+.root {
+ -fx-font-family: "JetBrains Mono";
+}
diff --git a/launcher/src/main/resources/main_view.fxml b/launcher/src/main/resources/main_view.fxml
index 7566a20..ef98412 100644
--- a/launcher/src/main/resources/main_view.fxml
+++ b/launcher/src/main/resources/main_view.fxml
@@ -1,64 +1,51 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/src/main/resources/styles.css b/launcher/src/main/resources/styles.css
index e69de29..e2a53b6 100644
--- a/launcher/src/main/resources/styles.css
+++ b/launcher/src/main/resources/styles.css
@@ -0,0 +1,22 @@
+.test{
+ -fx-background-color: blue;
+}
+
+.button-bar {
+ -fx-padding: 5 0 5 0;
+ -fx-spacing: 5;
+ -fx-font-weight: bold;
+ -fx-font-size: 16px;
+}
+
+.banner-list {
+ -fx-spacing: 5;
+}
+
+.list-item:selected {
+ -fx-background-color: #e3e3e3;
+}
+
+#playButton {
+ -fx-border-radius: 0;
+}