Added lots of stuff:
- custom font - profile editor - servers list - custom data directory management.
This commit is contained in:
parent
4db1ecd191
commit
8cfdf32bc0
|
@ -26,6 +26,12 @@
|
||||||
<artifactId>javafx-fxml</artifactId>
|
<artifactId>javafx-fxml</artifactId>
|
||||||
<version>${javafx.version}</version>
|
<version>${javafx.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.9.1</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -4,6 +4,10 @@ module aos2_launcher {
|
||||||
requires javafx.graphics;
|
requires javafx.graphics;
|
||||||
requires javafx.fxml;
|
requires javafx.fxml;
|
||||||
|
|
||||||
|
requires java.net.http;
|
||||||
|
requires com.google.gson;
|
||||||
|
|
||||||
exports nl.andrewl.aos2_launcher to javafx.graphics;
|
exports nl.andrewl.aos2_launcher to javafx.graphics;
|
||||||
opens nl.andrewl.aos2_launcher to javafx.fxml;
|
opens nl.andrewl.aos2_launcher to javafx.fxml;
|
||||||
|
opens nl.andrewl.aos2_launcher.view to javafx.fxml;
|
||||||
}
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package nl.andrewl.aos2_launcher;
|
||||||
|
|
||||||
|
public class EditProfileController {
|
||||||
|
}
|
|
@ -7,22 +7,41 @@ import javafx.scene.Scene;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main starting point for the launcher app.
|
* The main starting point for the launcher app.
|
||||||
*/
|
*/
|
||||||
public class Launcher extends Application {
|
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
|
@Override
|
||||||
public void start(Stage stage) throws IOException {
|
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"));
|
FXMLLoader loader = new FXMLLoader(Launcher.class.getResource("/main_view.fxml"));
|
||||||
Parent rootNode = loader.load();
|
Parent rootNode = loader.load();
|
||||||
Scene scene = new Scene(rootNode);
|
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.setScene(scene);
|
||||||
stage.setTitle("Ace of Shades 2 - Launcher");
|
stage.setTitle("Ace of Shades 2 - Launcher");
|
||||||
stage.show();
|
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) {
|
public static void main(String[] args) {
|
||||||
launch(args);
|
launch(args);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,216 @@
|
||||||
package nl.andrewl.aos2_launcher;
|
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.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.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 {
|
public class MainViewController {
|
||||||
@FXML
|
@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<Profile> profiles = FXCollections.observableArrayList();
|
||||||
|
private final ObservableList<Server> servers = FXCollections.observableArrayList();
|
||||||
|
private final ObjectProperty<Server> selectedServer = new SimpleObjectProperty<>(null);
|
||||||
|
private final ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
public void onExit(ActionEvent actionEvent) {
|
private final ServersFetcher serversFetcher = new ServersFetcher();
|
||||||
Platform.exit();
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<List<Server>> 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<Server> 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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<LocalDateTime> 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<LocalDateTime> lastUpdatedAtProperty() {
|
||||||
|
return lastUpdatedAt;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <E, F> void mapContent(ObservableList<F> mapped, ObservableList<? extends E> source,
|
||||||
|
Function<? super E, ? extends F> mapper) {
|
||||||
|
map(mapped, source, mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <E, F> Object map(ObservableList<F> mapped, ObservableList<? extends E> source,
|
||||||
|
Function<? super E, ? extends F> mapper) {
|
||||||
|
final ListContentMapping<E, F> 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<E, F> implements ListChangeListener<E>, WeakListener {
|
||||||
|
private final WeakReference<List<F>> mappedRef;
|
||||||
|
private final Function<? super E, ? extends F> mapper;
|
||||||
|
|
||||||
|
public ListContentMapping(List<F> 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<F> 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<F> list = mappedRef.get();
|
||||||
|
return (list == null) ? 0 : list.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<F> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Profile> {
|
||||||
|
@FXML
|
||||||
|
public TextField nameField;
|
||||||
|
@FXML
|
||||||
|
public TextArea descriptionTextArea;
|
||||||
|
@FXML
|
||||||
|
public ChoiceBox<String> clientVersionChoiceBox;
|
||||||
|
|
||||||
|
private final ObjectProperty<Profile> 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<AnchorPane minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" spacing="10.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||||
|
</padding>
|
||||||
|
<AnchorPane VBox.vgrow="NEVER">
|
||||||
|
<Label text="Name" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||||
|
<TextField fx:id="nameField" promptText="Enter a name for the profile..." AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="150.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||||
|
</AnchorPane>
|
||||||
|
<AnchorPane VBox.vgrow="NEVER">
|
||||||
|
<Label text="Description" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||||
|
<TextArea fx:id="descriptionTextArea" prefHeight="100.0" prefWidth="200.0" promptText="Add a description for this profile..." wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="150.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||||
|
</AnchorPane>
|
||||||
|
<AnchorPane VBox.vgrow="NEVER">
|
||||||
|
<Label text="Client Version" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||||
|
<ChoiceBox fx:id="clientVersionChoiceBox" prefWidth="150.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="150.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||||
|
</AnchorPane>
|
||||||
|
</VBox>
|
||||||
|
</AnchorPane>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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";
|
||||||
|
}
|
|
@ -1,64 +1,51 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
<?import javafx.geometry.*?>
|
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
|
<VBox
|
||||||
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="nl.andrewl.aos2_launcher.MainViewController">
|
maxHeight="-Infinity"
|
||||||
<MenuBar>
|
maxWidth="-Infinity"
|
||||||
<Menu mnemonicParsing="false" text="File">
|
minHeight="300.0"
|
||||||
<MenuItem mnemonicParsing="false" onAction="#onExit" text="Exit" />
|
minWidth="300.0"
|
||||||
</Menu>
|
prefHeight="400.0"
|
||||||
<Menu mnemonicParsing="false" text="Profiles">
|
prefWidth="600.0"
|
||||||
<MenuItem mnemonicParsing="false" text="New Profile" />
|
xmlns="http://javafx.com/javafx/16"
|
||||||
</Menu>
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
<Menu mnemonicParsing="false" text="Help">
|
fx:controller="nl.andrewl.aos2_launcher.MainViewController"
|
||||||
<MenuItem mnemonicParsing="false" text="About" />
|
>
|
||||||
</Menu>
|
<TabPane tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
|
||||||
</MenuBar>
|
<Tab text="Profiles">
|
||||||
<SplitPane dividerPositions="0.5" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
<VBox>
|
||||||
<items>
|
<HBox VBox.vgrow="NEVER" alignment="CENTER" styleClass="button-bar">
|
||||||
<VBox minWidth="-Infinity" prefHeight="200.0" prefWidth="100.0">
|
<Button text="Add Profile" onAction="#addProfile"/>
|
||||||
<children>
|
<Button fx:id="editProfileButton" text="Edit Profile" onAction="#editProfile"/>
|
||||||
<HBox alignment="CENTER" prefWidth="200.0" VBox.vgrow="NEVER">
|
<Button fx:id="removeProfileButton" text="Remove Profile" onAction="#removeProfile"/>
|
||||||
<children>
|
|
||||||
<Label contentDisplay="CENTER" text="Profiles" />
|
|
||||||
</children>
|
|
||||||
</HBox>
|
</HBox>
|
||||||
<Separator prefWidth="200.0" />
|
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS">
|
||||||
<ScrollPane fitToWidth="true" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
<VBox fx:id="profilesVBox" styleClass="banner-list"/>
|
||||||
<content>
|
|
||||||
<VBox fx:id="profilesVBox" prefWidth="100.0" />
|
|
||||||
</content>
|
|
||||||
</ScrollPane>
|
</ScrollPane>
|
||||||
</children>
|
|
||||||
</VBox>
|
</VBox>
|
||||||
<VBox minWidth="-Infinity" prefHeight="200.0" prefWidth="100.0">
|
</Tab>
|
||||||
<children>
|
<Tab text="Servers">
|
||||||
<HBox alignment="CENTER" prefWidth="200.0" VBox.vgrow="NEVER">
|
<VBox>
|
||||||
<children>
|
<HBox VBox.vgrow="NEVER" alignment="CENTER" styleClass="button-bar">
|
||||||
<Label text="Game Versions" />
|
<Button text="Refresh" onAction="#refreshServers"/>
|
||||||
</children>
|
|
||||||
</HBox>
|
</HBox>
|
||||||
<Separator prefWidth="200.0" />
|
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS">
|
||||||
<ScrollPane fitToWidth="true" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
<VBox fx:id="serversVBox" styleClass="banner-list"/>
|
||||||
<content>
|
|
||||||
<VBox prefWidth="100.0" />
|
|
||||||
</content>
|
|
||||||
</ScrollPane>
|
</ScrollPane>
|
||||||
</children>
|
|
||||||
</VBox>
|
</VBox>
|
||||||
</items>
|
</Tab>
|
||||||
</SplitPane>
|
</TabPane>
|
||||||
<HBox alignment="CENTER" prefWidth="200.0" VBox.vgrow="NEVER">
|
<HBox alignment="CENTER" VBox.vgrow="NEVER" styleClass="button-bar">
|
||||||
<children>
|
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" spacing="5">
|
||||||
<Button fx:id="playButton" mnemonicParsing="false" text="Play" />
|
<Label text="Profile: "/>
|
||||||
</children>
|
<Label fx:id="selectedProfileLabel" text="None"/>
|
||||||
<opaqueInsets>
|
</HBox>
|
||||||
<Insets />
|
<Button fx:id="playButton" mnemonicParsing="false" text="Play" onAction="#play"/>
|
||||||
</opaqueInsets>
|
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" spacing="5">
|
||||||
<padding>
|
<Label text="Server: "/>
|
||||||
<Insets bottom="5.0" top="5.0" />
|
<Label fx:id="selectedServerLabel" text="None"/>
|
||||||
</padding>
|
</HBox>
|
||||||
</HBox>
|
</HBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue