Commit b870062e authored by Nico Eckes's avatar Nico Eckes

Moved editor code to own module. Further editor code

parent d30daf52
Pipeline #742 passed with stage
in 3 minutes and 40 seconds
package com.unitedworldminers.permio.editor;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.stream.Collectors;
public abstract class ExtendedList<T> extends VBox {
@FXML public Label title;
@FXML public TextField searchField;
@FXML public TextField addField;
@FXML public ListView<T> list;
public final ObservableList<T> items = FXCollections.observableArrayList();
public ExtendedList() {
FXMLLoader loader = new FXMLLoader(getClass().getResource("ExtendedList.fxml"));
loader.setController(this);
try {
loader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
@FXML
public void initialize() {
FilteredList<T> filteredItems = new FilteredList<>(items, t-> true);
list.setItems(filteredItems.sorted(comparator()));
searchField.textProperty().addListener(obs -> {
String filter = searchField.getText();
filteredItems.setPredicate(filter.isEmpty() ? s -> true : s -> s.toString().toLowerCase().contains(filter.toLowerCase()));
});
list.setCellFactory(param -> new RemoveableEntry());
list.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.DELETE) {
for (T sel : new ArrayList<>(list.getSelectionModel().getSelectedItems())) {
items.remove(sel);
}
return;
}
if (event.getCode() == KeyCode.C && event.isControlDown()) {
String s = list.getSelectionModel().getSelectedItems().stream().map(Object::toString).collect(Collectors.joining("\n"));
if (!s.isEmpty())
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(s), null);
return;
}
if (event.getCode() == KeyCode.V && event.isControlDown()) {
try {
String paste = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
if (paste == null) return;
for (T item: provideItem(paste.split("\n"))) {
if (item != null) items.add(item);
}
} catch (UnsupportedFlavorException | IOException ignored) {
}
return;
}
});
addField.textProperty().addListener(obs -> addField.setStyle(null));
getChildren().addAll(title, searchField, list, addField);
}
public void reset() {
searchField.clear();
items.clear();
addField.clear();
}
@FXML
private void addFieldKeyPress(KeyEvent event) {
if (event.getCode() == KeyCode.ENTER) {
String text = addField.getText().trim();
if (text.isEmpty()) return;
addField.setStyle("-fx-text-fill: orange");
new Thread(()-> {
T item = provideItem(text)[0];
if (item == null) {
Platform.runLater(()->addField.setStyle("-fx-text-fill: red"));
return;
}
Platform.runLater(()->{
items.add(item);
addField.clear();
});
}).start();
}
}
abstract Comparator<T> comparator();
abstract T[] provideItem(String... text);
abstract Node provideNode(T item);
class RemoveableEntry extends ListCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
return;
}
setMinWidth(0);
setPrefWidth(1);
ImageView removeBtn = new ImageView(new Image(getClass().getResourceAsStream("img/remove.png")));
removeBtn.setOnMouseClicked(event -> items.remove(item));
Node node = provideNode(item);
setGraphic(new StackPane(node, removeBtn));
StackPane.setAlignment(node, Pos.CENTER_LEFT);
StackPane.setAlignment(removeBtn, Pos.CENTER_RIGHT);
}
}
}
package com.unitedworldminers.permio.editor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import java.util.Comparator;
public class GroupsList extends ExtendedList<String> {
@Override
public void initialize() {
super.initialize();
title.setText("Groups");
}
@Override
Comparator<String> comparator() {
return Comparator.naturalOrder();
}
@Override
String[] provideItem(String... text) {
return text;
}
@Override
Node provideNode(String item) {
return new Label(item);
}
}
package com.unitedworldminers.permio.editor;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitPane;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.representer.Representer;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
public class GroupsScreen {
@FXML private MenuBar menuBar;
@FXML private SplitPane content;
@FXML private GroupsList groups;
@FXML private PermissionList permissions;
@FXML private PlayerList players;
private Path groupsFile;
private Path playersFile;
private Yaml yaml;
private Map<String, Map<String, Object>> groupdata = null;
private Map<String, Map<String, Object>> playerdata = null;
@FXML
private void initialize() {
DumperOptions yamlOptions = new DumperOptions();
yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
yamlOptions.setAllowUnicode(true);
Representer yamlRepresenter = new Representer();
yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
yaml = new Yaml(yamlRepresenter, yamlOptions);
for (Menu m: menuBar.getMenus()) {
m.getItems().add(new MenuItem());
m.addEventHandler(Menu.ON_SHOWN, event -> {
m.hide();
m.fire();
});
}
loadFromFile(Paths.get("groups.yml"), Paths.get("players.yml"));
}
@FXML
private void openFileDialog(ActionEvent actionEvent) {
}
@FXML
private void reload(ActionEvent actionEvent) {
reload();
}
public void loadFromFile(Path groupsFile, Path playersFile) {
Path gsave = this.groupsFile;
Path psave = this.playersFile;
this.groupsFile = groupsFile;
this.playersFile = playersFile;
if (reload() != null) {
this.groupsFile = gsave;
this.playersFile = psave;
}
}
@Nullable
private String reload() {
Map<String, Map<String, Object>> groupdata = loadYaml(groupsFile);
if (groupdata == null) return "Could not load groups from "+groupsFile.toString();
Map<String, Map<String, Object>> playerdata = loadYaml(playersFile);
if (playerdata == null) return "Could not load players from "+playersFile.toString();
this.groupdata = groupdata;
this.playerdata = playerdata;
groups.reset();
permissions.reset();
players.reset();
groups.items.addAll(groupdata.keySet());
return null;
}
@Nullable
private Map<String, Map<String,Object>> loadYaml(Path file) {
try (BufferedReader br = Files.newBufferedReader(file)) {
return yaml.loadAs(br, YamlType.class);
} catch (IOException e) {
return null;
}
}
private abstract static class YamlType implements Map<String, Map<String, Object>> {
}
}
package com.unitedworldminers.permio.editor;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("GroupsScreen.fxml"));
primaryStage.setTitle("Permission Editor");
primaryStage.getIcons().add(new Image(getClass().getClassLoader().getResourceAsStream("com/unitedworldminers/permio/editor/img/PermIO.png")));
primaryStage.setScene(new Scene(root, 1000, 500));
primaryStage.show();
}
}
package com.unitedworldminers.permio.editor;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Pair;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Random;
import java.util.stream.Stream;
public class PermissionList extends ExtendedList<String> {
@Override
public void initialize() {
super.initialize();
title.setText("Permissions");
}
@Override
Comparator<String> comparator() {
return Comparator.naturalOrder();
}
@Override
String[] provideItem(String... text) {
return text;
}
@Override
Node provideNode(String item) {
return new PermissionLabel(item);
}
private static class PermissionLabel extends TextFlow {
private static final Random random = new Random();
private static final ColorMap colors = new ColorMap();
private final String text;
public PermissionLabel(String text) {
this.text = text;
final ColorMap[] map = new ColorMap[]{colors};
String[] parts = text.split("\\.");
if (parts.length == 0) {
getChildren().add(new Text(text));
} else {
getChildren().addAll(Stream.of(parts).flatMap(s -> {
Text t = new Text(s);
Pair<Color, ColorMap> data = map[0].computeIfAbsent(s, s1 -> new Pair<>(nextColor(), new ColorMap()));
map[0] = data.getValue();
t.setFill(data.getKey());
return Stream.of(t, new Text("."));
}).limit(parts.length * 2 - 1).toArray(Text[]::new));
}
}
@Override
public String toString() {
return text;
}
@NotNull
private static Color nextColor() {
return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256));
}
private static class ColorMap extends HashMap<String, Pair<Color, ColorMap>> {
}
}
}
package com.unitedworldminers.permio.editor;
import javafx.scene.image.Image;
import org.jetbrains.annotations.Contract;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class PlayerData {
private static final Pattern pattern1 = Pattern.compile("(\\w{32}).*(?<=name\":\")(\\w*)");
private static final Pattern pattern2 = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})");
private static final String replace2 = "$1-$2-$3-$4-$5";
public static final Map<String, PlayerData> cache = new HashMap<>();
public static final PlayerData NULL = new PlayerData();
public final UUID uuid;
public final String name;
public final Image image;
public static PlayerData[] query(String... any) {
UUID[] uuids = new UUID[any.length];
for (int i = 0; i < any.length; i++) {
try {
uuids[i] = UUID.fromString(pattern2.matcher(any[i]).replaceFirst(replace2));
any[i] = null;
} catch (IllegalArgumentException ignored) {
}
}
PlayerData[] result = of(uuids);
PlayerData[] add = of(any);
for (int i = 0; i < any.length; i++) if (result[i] == null) result[i] = add[i];
return result;
}
private PlayerData() {
uuid = null;
name = null;
image = null;
}
private PlayerData(UUID uuid, String name) {
this.uuid = uuid;
this.name = name;
this.image = new Image("https://crafatar.com/avatars/" + uuid.toString().replace("-", ""), true);
cache.put(uuid.toString(), this);
cache.put(name, this);
}
public String text() {
return name + " (" + uuid.toString() + ')';
}
@Contract(value = "null -> false", pure = true)
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PlayerData)) return false;
PlayerData o = (PlayerData) obj;
return Objects.equals(name, o.name) && Objects.equals(uuid, o.uuid);
}
public static PlayerData[] of(String... names) {
PlayerData[] result = new PlayerData[names.length];
for (int i = 0; i < names.length; i += 100) {
for (int j = 0; j < 100 && i + j < names.length; j++) result[i + j] = names[i + j] == null ? null : cache.get(names[i + j].toLowerCase());
String request = IntStream.range(i, Math.min(i + 100, names.length)).filter(x->result[x] == null && names[x] != null).mapToObj(x -> '"' + names[x] + '"').collect(Collectors.joining(",", "[", "]"));
if (request.equals("[]")) continue;
System.out.println("Requesting names: " + request);
try {
HttpURLConnection connection = (HttpURLConnection) new URL("https://api.mojang.com/profiles/minecraft").openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
connection.connect();
try (OutputStream os = connection.getOutputStream()) {
os.write(request.getBytes());
}
Map<String, PlayerData> response;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
response = Stream.of(reader.lines().collect(Collectors.joining()).replace("\n", "").replace(" ", "").replace("\t", "").split("},\\{")).map(s -> {
Matcher m = pattern1.matcher(s);
return m.find() ? new PlayerData(UUID.fromString(pattern2.matcher(m.group(1)).replaceFirst(replace2)), m.group(2)) : null;
}).filter(Objects::nonNull).collect(Collectors.toMap(p -> p.name.toLowerCase(), Function.identity()));
}
System.out.println("Got: " + response);
for (int j = 0; j < 100 && i + j < names.length; j++) {
if (result[i + j] != null || names[i + j] == null) continue;
PlayerData entry = response.get(names[i + j].toLowerCase());
if (entry == null) {
cache.put(names[i + j], NULL);
} else {
result[i + j] = entry;
}
}
} catch (IOException ignored) {
}
}
for (int i = 0; i < result.length; i++) if (result[i] == NULL) result[i] = null;
return result;
}
public static PlayerData[] of(UUID... uuids) {
PlayerData[] result = new PlayerData[uuids.length];
for (int i = 0; i < uuids.length; i++) {
if (uuids[i] == null) {
result[i] = null;
continue;
}
String uuid = uuids[i].toString();
PlayerData cached = cache.get(uuid);
if (cached != null) {
result[i] = cached == NULL ? null : cached;
continue;
}
System.out.println("Requesting uuid: " + uuid);
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new URL("https://api.mojang.com/user/profiles/" + uuid.replace("-", "") + "/names").openStream()))) {
String json = reader.lines().collect(Collectors.joining("\n"));
int end = json.lastIndexOf(',');
json = json.substring(json.lastIndexOf("name"), end == -1 ? json.length() - 2 : end);
String name = json.substring(7, json.length() - 1);
System.out.println("Got: " + name);
result[i] = new PlayerData(uuids[i], name);
}
} catch (IOException | StringIndexOutOfBoundsException e) {
cache.put(uuid, NULL);
result[i] = null;
}
}
return result;
}
@Override
public String toString() {
return text();
}
}
package com.unitedworldminers.permio.editor;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import java.util.Comparator;
public class PlayerList extends ExtendedList<PlayerData> {
@Override
public void initialize() {
super.initialize();
title.setText("Players");
}
@Override
Comparator<PlayerData> comparator() {
return Comparator.comparing(d->d == null ? null : d.uuid);
}
@Override
PlayerData[] provideItem(String... text) {
return PlayerData.query(text);
}
@Override
Node provideNode(PlayerData item) {
return new PlayerLabel(item);
}
private static class PlayerLabel extends HBox {
public PlayerLabel(PlayerData pd) {
ImageView avatar = new ImageView();
avatar.setPreserveRatio(true);
avatar.setFitHeight(16);
avatar.setImage(pd.image);
getChildren().add(avatar);
Label name = new Label(pd.text());
name.setPadding(new Insets(0,0,0,3));
getChildren().add(name);
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
<Label fx:id="title">
<padding>
<Insets left="3.0"/>
</padding>
</Label>
<TextField fx:id="searchField" promptText="Search..."/>
<ListView fx:id="list" VBox.vgrow="ALWAYS"/>
<TextField fx:id="addField" onKeyPressed="#addFieldKeyPress"/>
</VBox>
<?xml version="1.0" encoding="UTF-8"?>
<?import com.unitedworldminers.permio.editor.GroupsList?>
<?import com.unitedworldminers.permio.editor.PermissionList?>
<?import com.unitedworldminers.permio.editor.PlayerList?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.SplitPane?>
<?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/8.0.121" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.unitedworldminers.permio.editor.EditorMain">
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.unitedworldminers.permio.editor.GroupsScreen">
<MenuBar fx:id="menuBar">
<Menu mnemonicParsing="false" onAction="#openFileDialog" text="Load"/>