Update from Git Manager GUI

This commit is contained in:
2026-03-24 08:54:30 +01:00
parent 6edf8f9157
commit e4b2ac32ca
18 changed files with 2748 additions and 265 deletions

View File

@@ -0,0 +1,326 @@
package me.viper.teamplugin.gui;
import me.viper.teamplugin.Main;
import me.viper.teamplugin.manager.ApplicationManager;
import me.viper.teamplugin.manager.AuditLog;
import me.viper.teamplugin.manager.DataManager;
import me.viper.teamplugin.manager.LangManager;
import me.viper.teamplugin.util.Utils;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import java.util.*;
/**
* ApplicationGUI Admin-only GUI for reviewing and managing pending applications.
*
* LIST VIEW (54 slots) /team apply [admin only]
* ┌───────────────────────────────────┐
* │ Header (cyan glass) │
* │ Applicant heads slots 9-44 │ one PLAYER_HEAD per pending application
* │ Footer: ←45 | close 49 | →53 │
* └───────────────────────────────────┘
*
* DETAIL VIEW (54 slots)
* ┌───────────────────────────────────┐
* │ Background filler │
* │ slot 13 Applicant head + info │
* │ slot 20 Accept (GREEN) │
* │ slot 24 Deny (RED) │
* │ slot 49 Back │
* └───────────────────────────────────┘
*
* Players apply purely via command: /team bewerben <rank> [reason]
* No GUI is shown to the applicant.
*/
public class ApplicationGUI {
private static final int ITEMS_PER_PAGE = 36;
/** UUID → current list-view page */
private static final Map<UUID, Integer> CURRENT_PAGE = new HashMap<>();
/** UUID → applicant name currently shown in the detail view */
private static final Map<UUID, String> VIEWING_PLAYER = new HashMap<>();
// ── Open: list view ───────────────────────────────────────────────
public static void openApplicationList(Player admin, int page) {
List<String[]> all = ApplicationManager.getAllApplications(); // [rank, name, iso, reason]
int totalPages = Math.max(1, (int) Math.ceil((double) all.size() / ITEMS_PER_PAGE));
page = Math.max(0, Math.min(page, totalPages - 1));
CURRENT_PAGE.put(admin.getUniqueId(), page);
Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getSelectionTitle()));
// Header row
ItemStack hGlass = filler(Material.CYAN_STAINED_GLASS_PANE);
for (int i = 0; i < 9; i++) inv.setItem(i, hGlass);
// Footer row
ItemStack fGlass = filler(Material.BLACK_STAINED_GLASS_PANE);
for (int i = 45; i < 54; i++) inv.setItem(i, fGlass);
// Info item (slot 4)
inv.setItem(4, buildInfoItem(all.size(), page + 1, totalPages));
// Applicant items (slots 9-44)
int start = page * ITEMS_PER_PAGE;
for (int i = 0; i < ITEMS_PER_PAGE; i++) {
int idx = start + i;
if (idx >= all.size()) break;
inv.setItem(9 + i, buildApplicantItem(all.get(idx)));
}
// Navigation
if (page > 0) inv.setItem(45, navItem(Material.ARROW,
Utils.color(LangManager.get("mailbox_prev_page")), List.of()));
inv.setItem(49, navItem(Material.BARRIER,
Utils.color(LangManager.get("apply_gui_close")), List.of()));
if (page + 1 < totalPages) inv.setItem(53, navItem(Material.ARROW,
Utils.color(LangManager.get("mailbox_next_page")), List.of()));
admin.openInventory(inv);
}
// ── Open: detail view ─────────────────────────────────────────────
private static void openDetail(Player admin, String[] app) {
// app: [rank, name, iso, reason]
VIEWING_PLAYER.put(admin.getUniqueId(), app[1]);
Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getDetailTitle()));
ItemStack bg = filler(Material.GRAY_STAINED_GLASS_PANE);
for (int i = 0; i < 54; i++) inv.setItem(i, bg);
// Applicant head + full info (slot 13)
inv.setItem(13, buildDetailItem(app));
// Accept (slot 20)
String rankDisplay = Main.getInstance().getConfig()
.getString("rank-settings." + app[0] + ".display", app[0]);
inv.setItem(20, navItem(Material.GREEN_CONCRETE,
Utils.color("&a&l" + LangManager.get("apply_accepted_btn")),
List.of(Utils.color("&7" + app[1] + " &8→ " + rankDisplay))));
// Deny (slot 24)
inv.setItem(24, navItem(Material.RED_CONCRETE,
Utils.color("&c&l" + LangManager.get("apply_denied_btn")),
List.of(Utils.color("&7" + LangManager.get("apply_deny_lore")))));
// Back (slot 49)
inv.setItem(49, navItem(Material.ARROW,
Utils.color(LangManager.get("mailbox_back_btn")), List.of()));
admin.openInventory(inv);
}
// ── Click handlers ────────────────────────────────────────────────
public static void handleSelectionClick(Player admin, InventoryClickEvent e) {
e.setCancelled(true);
int slot = e.getRawSlot();
ItemStack clicked = e.getCurrentItem();
if (clicked == null || clicked.getType().isAir()) return;
int page = CURRENT_PAGE.getOrDefault(admin.getUniqueId(), 0);
switch (slot) {
case 45 -> openApplicationList(admin, page - 1);
case 49 -> { cleanup(admin.getUniqueId()); admin.closeInventory(); }
case 53 -> openApplicationList(admin, page + 1);
default -> {
if (slot >= 9 && slot <= 44) {
List<String[]> all = ApplicationManager.getAllApplications();
int idx = page * ITEMS_PER_PAGE + (slot - 9);
if (idx < all.size()) openDetail(admin, all.get(idx));
}
}
}
}
public static void handleDetailClick(Player admin, InventoryClickEvent e) {
e.setCancelled(true);
int slot = e.getRawSlot();
ItemStack clicked = e.getCurrentItem();
if (clicked == null || clicked.getType().isAir()) return;
String applicantName = VIEWING_PLAYER.get(admin.getUniqueId());
if (applicantName == null) { admin.closeInventory(); return; }
int page = CURRENT_PAGE.getOrDefault(admin.getUniqueId(), 0);
String appliedRank = ApplicationManager.findApplication(applicantName);
switch (slot) {
case 20 -> { // Accept
if (appliedRank != null) {
ApplicationManager.removeApplication(applicantName);
DataManager.addMember(appliedRank, applicantName);
AuditLog.log(AuditLog.APPLY_ACCEPT, admin.getName(),
applicantName + " \u2192 " + appliedRank);
admin.sendMessage(Utils.color(Utils.replace(
LangManager.get("apply_accepted"),
"%player%", applicantName, "%rank%", appliedRank)));
Player applicant = Bukkit.getPlayerExact(applicantName);
if (applicant != null) applicant.sendMessage(Utils.color(
Utils.replace(LangManager.get("apply_you_accepted"),
"%rank%", appliedRank)));
}
VIEWING_PLAYER.remove(admin.getUniqueId());
openApplicationList(admin, page);
}
case 24 -> { // Deny
if (appliedRank != null) {
ApplicationManager.removeApplication(applicantName);
AuditLog.log(AuditLog.APPLY_DENY, admin.getName(),
applicantName + " for " + appliedRank);
admin.sendMessage(Utils.color(Utils.replace(
LangManager.get("apply_denied"),
"%player%", applicantName)));
Player applicant = Bukkit.getPlayerExact(applicantName);
if (applicant != null) applicant.sendMessage(Utils.color(
Utils.replace(LangManager.get("apply_you_denied"),
"%rank%", appliedRank)));
}
VIEWING_PLAYER.remove(admin.getUniqueId());
openApplicationList(admin, page);
}
case 49 -> { // Back
VIEWING_PLAYER.remove(admin.getUniqueId());
openApplicationList(admin, page);
}
}
}
// ── Title helpers ─────────────────────────────────────────────────
public static String getSelectionTitle() {
return LangManager.get("apply_gui_selection_title");
}
public static String getDetailTitle() {
return LangManager.get("apply_gui_detail_title");
}
public static boolean isSelectionTitle(String colored) {
return colored.equals(Utils.color(getSelectionTitle()));
}
public static boolean isDetailTitle(String colored) {
return colored.equals(Utils.color(getDetailTitle()));
}
// ── Cleanup ────────────────────────────────────────────────────────
public static void cleanup(UUID uuid) {
CURRENT_PAGE.remove(uuid);
VIEWING_PLAYER.remove(uuid);
}
// ── Stubs kept so ChatListener compiles without changes ────────────
// (The old player-text-input flow has been removed.)
public static boolean isAwaitingText(UUID uuid) { return false; }
public static boolean handleTextInput(Player player, String text) { return false; }
// ── Item builders ─────────────────────────────────────────────────
private static ItemStack buildInfoItem(int total, int page, int totalPages) {
ItemStack item = new ItemStack(Material.ENCHANTED_BOOK);
ItemMeta m = item.getItemMeta();
if (m != null) {
m.setDisplayName(Utils.color("&b&lBewerbungen"));
m.setLore(List.of(
Utils.color("&7Ausstehend&8: &e" + total),
Utils.color("&7Seite &e" + page + " &7/ &e" + totalPages)
));
item.setItemMeta(m);
}
return item;
}
private static ItemStack buildApplicantItem(String[] app) {
// app: [rank, name, iso, reason]
ItemStack item = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) item.getItemMeta();
if (meta != null) {
meta.setOwningPlayer(Bukkit.getOfflinePlayer(app[1]));
meta.setDisplayName(Utils.color("&e&l" + app[1]));
String rankDisplay = Main.getInstance().getConfig()
.getString("rank-settings." + app[0] + ".display", app[0]);
List<String> lore = new ArrayList<>();
lore.add(Utils.color("&7Rang&8: " + rankDisplay));
lore.add(Utils.color("&7Datum&8: &f" + Utils.prettifyIso(app[2])));
if (!app[3].equals("-")) lore.add(Utils.color("&7Grund&8: &f" + app[3]));
lore.add("");
lore.add(Utils.color("&aKlicken zum Bearbeiten"));
meta.setLore(lore);
item.setItemMeta(meta);
}
return item;
}
private static ItemStack buildDetailItem(String[] app) {
// app: [rank, name, iso, reason]
ItemStack item = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) item.getItemMeta();
if (meta != null) {
meta.setOwningPlayer(Bukkit.getOfflinePlayer(app[1]));
meta.setDisplayName(Utils.color("&e&l" + app[1]));
String rankDisplay = Main.getInstance().getConfig()
.getString("rank-settings." + app[0] + ".display", app[0]);
List<String> lore = new ArrayList<>();
lore.add(Utils.color("&7Rang&8: " + rankDisplay));
lore.add(Utils.color("&7Datum&8: &f" + Utils.prettifyIso(app[2])));
lore.add("");
if (app[3].equals("-")) {
lore.add(Utils.color("&8Kein Bewerbungstext angegeben"));
} else {
lore.add(Utils.color("&7Bewerbungstext&8:"));
for (String line : wrapText(app[3], 35))
lore.add(Utils.color("&f " + line));
}
meta.setLore(lore);
item.setItemMeta(meta);
}
return item;
}
// ── Generic helpers ───────────────────────────────────────────────
private static ItemStack filler(Material mat) {
ItemStack item = new ItemStack(mat);
ItemMeta m = item.getItemMeta();
if (m != null) { m.setDisplayName(" "); item.setItemMeta(m); }
return item;
}
private static ItemStack navItem(Material mat, String name, List<String> lore) {
ItemStack item = new ItemStack(mat);
ItemMeta m = item.getItemMeta();
if (m != null) { m.setDisplayName(name); m.setLore(lore); item.setItemMeta(m); }
return item;
}
private static List<String> wrapText(String text, int maxChars) {
List<String> lines = new ArrayList<>();
String[] words = text.split(" ");
StringBuilder cur = new StringBuilder();
for (String w : words) {
if (cur.length() > 0 && cur.length() + 1 + w.length() > maxChars) {
lines.add(cur.toString());
cur = new StringBuilder(w);
} else {
if (cur.length() > 0) cur.append(' ');
cur.append(w);
}
}
if (cur.length() > 0) lines.add(cur.toString());
return lines.isEmpty() ? List.of(text) : lines;
}
}

View File

@@ -0,0 +1,313 @@
package me.viper.teamplugin.gui;
import me.viper.teamplugin.manager.LangManager;
import me.viper.teamplugin.manager.MailboxManager;
import me.viper.teamplugin.manager.MessageManager;
import me.viper.teamplugin.util.Utils;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.*;
/**
* MailboxGUI two-view postfach for TeamPlugin.
*
* LIST VIEW (54 slots)
* ┌─────────────────────────┐
* │ Header row (glass) │ slot 4 = mailbox info item
* │ Message items 9-44 │ WRITTEN_BOOK=unread, BOOK=read
* │ Footer nav 45/49/53 │ ← page | close | page →
* └─────────────────────────┘
*
* READ VIEW (54 slots)
* ┌─────────────────────────┐
* │ Background filler │
* │ Full message at 22 │
* │ Delete:20 Reply:24 │
* │ Back:49 │
* └─────────────────────────┘
*/
public class MailboxGUI {
private static final int ITEMS_PER_PAGE = 36; // rows 1-4, slots 9-44
/** UUID → current list-view page number */
private static final Map<UUID, Integer> CURRENT_PAGE = new HashMap<>();
/** UUID → whose mailbox is open (player name) */
private static final Map<UUID, String> OPEN_FOR_PLAYER = new HashMap<>();
/** UUID → msgId of message currently being read */
private static final Map<UUID, String> READING_ID = new HashMap<>();
// ── Open: list view ───────────────────────────────────────────────
/** Opens the caller's own mailbox at page 0. */
public static void openMailbox(Player viewer) {
openMailbox(viewer, viewer.getName(), 0);
}
/** Opens a specific player's mailbox at a given page (admins can inspect others). */
public static void openMailbox(Player viewer, String targetPlayer, int page) {
List<String[]> messages = MailboxManager.getParsedMessages(targetPlayer);
int totalPages = Math.max(1, (int) Math.ceil((double) messages.size() / ITEMS_PER_PAGE));
page = Math.max(0, Math.min(page, totalPages - 1));
OPEN_FOR_PLAYER.put(viewer.getUniqueId(), targetPlayer);
CURRENT_PAGE.put(viewer.getUniqueId(), page);
Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getListTitle()));
// Header row
ItemStack headerGlass = filler(Material.CYAN_STAINED_GLASS_PANE);
for (int i = 0; i < 9; i++) inv.setItem(i, headerGlass);
// Footer row
ItemStack footerGlass = filler(Material.BLACK_STAINED_GLASS_PANE);
for (int i = 45; i < 54; i++) inv.setItem(i, footerGlass);
// Info item (slot 4)
int unread = MailboxManager.getUnreadCount(targetPlayer);
inv.setItem(4, buildInfoItem(targetPlayer, unread, page + 1, totalPages));
// Message items (slots 9-44)
int start = page * ITEMS_PER_PAGE;
for (int i = 0; i < ITEMS_PER_PAGE; i++) {
int idx = start + i;
if (idx >= messages.size()) break;
inv.setItem(9 + i, buildMessageListItem(messages.get(idx)));
}
// Navigation
if (page > 0) inv.setItem(45, navItem(Material.ARROW,
Utils.color(LangManager.get("mailbox_prev_page")),
List.of(Utils.color("&7Vorherige Seite"))));
inv.setItem(49, navItem(Material.BARRIER,
Utils.color(LangManager.get("mailbox_close")),
List.of(Utils.color("&7Postfach schließen"))));
if (page + 1 < totalPages) inv.setItem(53, navItem(Material.ARROW,
Utils.color(LangManager.get("mailbox_next_page")),
List.of(Utils.color("&7Nächste Seite"))));
viewer.openInventory(inv);
}
// ── Open: read view ───────────────────────────────────────────────
private static void openMessage(Player viewer, String[] parts) {
// parts: [id, from, isoTimestamp, read, text]
String msgId = parts[0];
String from = parts[1];
String date = Utils.prettifyIso(parts[2]);
String text = parts[4];
READING_ID.put(viewer.getUniqueId(), msgId);
String targetPlayer = OPEN_FOR_PLAYER.getOrDefault(viewer.getUniqueId(), viewer.getName());
MailboxManager.markRead(targetPlayer, msgId);
Inventory inv = Bukkit.createInventory(null, 54, Utils.color(getReadTitle()));
// Background
ItemStack bg = filler(Material.GRAY_STAINED_GLASS_PANE);
for (int i = 0; i < 54; i++) inv.setItem(i, bg);
// Message paper at slot 22
inv.setItem(22, buildFullMessageItem(from, date, text));
// Delete (slot 20)
inv.setItem(20, navItem(Material.RED_CONCRETE,
Utils.color(LangManager.get("mailbox_delete_btn")),
List.of(Utils.color("&7Nachricht endgültig löschen"))));
// Reply (slot 24)
inv.setItem(24, navItem(Material.WRITABLE_BOOK,
Utils.color(Utils.replace(LangManager.get("mailbox_reply_btn"), "%player%", from)),
List.of(Utils.color("&7An &b" + from + " &7antworten"))));
// Back (slot 49)
inv.setItem(49, navItem(Material.ARROW,
Utils.color(LangManager.get("mailbox_back_btn")),
List.of(Utils.color("&7Zurück zur Liste"))));
viewer.openInventory(inv);
}
// ── Click handlers ────────────────────────────────────────────────
public static void handleListClick(Player player, InventoryClickEvent e) {
e.setCancelled(true);
int slot = e.getRawSlot();
ItemStack clicked = e.getCurrentItem();
if (clicked == null || clicked.getType().isAir()) return;
String target = OPEN_FOR_PLAYER.getOrDefault(player.getUniqueId(), player.getName());
int page = CURRENT_PAGE.getOrDefault(player.getUniqueId(), 0);
switch (slot) {
case 45 -> openMailbox(player, target, page - 1);
case 49 -> {
cleanup(player.getUniqueId());
player.closeInventory();
}
case 53 -> openMailbox(player, target, page + 1);
default -> {
if (slot >= 9 && slot <= 44) {
List<String[]> messages = MailboxManager.getParsedMessages(target);
int idx = page * ITEMS_PER_PAGE + (slot - 9);
if (idx < messages.size()) openMessage(player, messages.get(idx));
}
}
}
}
public static void handleReadClick(Player player, InventoryClickEvent e) {
e.setCancelled(true);
int slot = e.getRawSlot();
ItemStack clicked = e.getCurrentItem();
if (clicked == null || clicked.getType().isAir()) return;
String target = OPEN_FOR_PLAYER.getOrDefault(player.getUniqueId(), player.getName());
String msgId = READING_ID.get(player.getUniqueId());
int page = CURRENT_PAGE.getOrDefault(player.getUniqueId(), 0);
switch (slot) {
case 20 -> { // delete
if (msgId != null) MailboxManager.delete(target, msgId);
READING_ID.remove(player.getUniqueId());
player.sendMessage(Utils.color(LangManager.get("mail_deleted")));
openMailbox(player, target, page);
}
case 24 -> { // reply
if (msgId != null) {
MailboxManager.getParsedMessages(target).stream()
.filter(a -> a[0].equals(msgId))
.findFirst()
.ifPresent(parts -> {
cleanup(player.getUniqueId());
player.closeInventory();
MessageManager.startReply(player, parts[1]);
});
}
}
case 49 -> { // back
READING_ID.remove(player.getUniqueId());
openMailbox(player, target, page);
}
}
}
// ── Title helpers ─────────────────────────────────────────────────
public static String getListTitle() {
return LangManager.get("mailbox_title");
}
public static String getReadTitle() {
return LangManager.get("mailbox_read_title");
}
public static boolean isListTitle(String colored) {
return colored.equals(Utils.color(getListTitle()));
}
public static boolean isReadTitle(String colored) {
return colored.equals(Utils.color(getReadTitle()));
}
// ── State cleanup ─────────────────────────────────────────────────
public static void cleanup(UUID uuid) {
CURRENT_PAGE.remove(uuid);
OPEN_FOR_PLAYER.remove(uuid);
READING_ID.remove(uuid);
}
// ── Item builders ─────────────────────────────────────────────────
private static ItemStack buildInfoItem(String owner, int unread, int page, int total) {
ItemStack item = new ItemStack(Material.ENCHANTED_BOOK);
ItemMeta m = item.getItemMeta();
if (m != null) {
m.setDisplayName(Utils.color("&b&lPostfach &8 &7" + owner));
m.setLore(List.of(
Utils.color("&7Ungelesen&8: &e" + unread),
Utils.color("&7Seite &e" + page + " &7/ &e" + total)
));
item.setItemMeta(m);
}
return item;
}
private static ItemStack buildMessageListItem(String[] parts) {
// parts: [id, from, iso, read, text]
boolean read = "true".equals(parts[3]);
ItemStack item = new ItemStack(read ? Material.BOOK : Material.WRITTEN_BOOK);
ItemMeta m = item.getItemMeta();
if (m != null) {
m.setDisplayName(Utils.color((read ? "&7" : "&e&l") + "Von: " + parts[1]));
String preview = parts[4].length() > 38 ? parts[4].substring(0, 38) + "" : parts[4];
m.setLore(List.of(
Utils.color("&7" + Utils.prettifyIso(parts[2])),
Utils.color("&f" + preview),
"",
Utils.color(read ? "&8Gelesen" : "&eUngelesen Klicken zum Lesen")
));
item.setItemMeta(m);
}
return item;
}
private static ItemStack buildFullMessageItem(String from, String date, String text) {
ItemStack item = new ItemStack(Material.PAPER);
ItemMeta m = item.getItemMeta();
if (m != null) {
m.setDisplayName(Utils.color("&bVon: &e" + from + " &8| &7" + date));
List<String> lore = new ArrayList<>();
lore.add("");
for (String line : wrapText(text, 40)) lore.add(Utils.color("&f" + line));
m.setLore(lore);
item.setItemMeta(m);
}
return item;
}
// ── Generic helpers ───────────────────────────────────────────────
private static ItemStack filler(Material mat) {
ItemStack item = new ItemStack(mat);
ItemMeta m = item.getItemMeta();
if (m != null) { m.setDisplayName(" "); item.setItemMeta(m); }
return item;
}
private static ItemStack navItem(Material mat, String name, List<String> lore) {
ItemStack item = new ItemStack(mat);
ItemMeta m = item.getItemMeta();
if (m != null) { m.setDisplayName(name); m.setLore(lore); item.setItemMeta(m); }
return item;
}
/** Word-wraps {@code text} at {@code maxChars} characters per line. */
private static List<String> wrapText(String text, int maxChars) {
List<String> lines = new ArrayList<>();
String[] words = text.split(" ");
StringBuilder cur = new StringBuilder();
for (String w : words) {
if (cur.length() > 0 && cur.length() + 1 + w.length() > maxChars) {
lines.add(cur.toString());
cur = new StringBuilder(w);
} else {
if (cur.length() > 0) cur.append(' ');
cur.append(w);
}
}
if (cur.length() > 0) lines.add(cur.toString());
return lines.isEmpty() ? List.of(text) : lines;
}
}

View File

@@ -3,6 +3,8 @@ package me.viper.teamplugin.gui;
import me.viper.teamplugin.Main;
import me.viper.teamplugin.manager.DataManager;
import me.viper.teamplugin.manager.LangManager;
import me.viper.teamplugin.manager.MessageManager;
import me.viper.teamplugin.util.SkinResolver;
import me.viper.teamplugin.util.Utils;
import org.bukkit.Bukkit;
import org.bukkit.Material;
@@ -13,106 +15,462 @@ import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.profile.PlayerTextures;
import java.util.ArrayList;
import java.util.List;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
/**
* TeamGUI two-layer navigation with optional pagination on rank pages.
*
* Layer 1 Overview one configurable icon per rank.
* Slot read from rank-settings.<rank>.slot (config.yml).
* Clicking opens the rank's member page.
*
* Layer 2 Rank Page shows player heads.
* If members exceed capacity → pagination.
* Footer layout (slots 45-53):
* 45 ← prev rank (wraps)
* 46 ← prev page (hidden if page 0)
* 49 home
* 52 → next page (hidden if last page)
* 53 → next rank (wraps)
*
* Page capacity:
* rank-settings.<rank>.member_slots defined → capacity = list size
* otherwise → gui.members_per_page (default 40)
*
* GUI size is always 54 and NOT configurable.
*/
public class TeamGUI {
// Zeilenpositionen für Ränge
private static final int[] rows = {1, 2, 3, 4};
private static final int GUI_SIZE = 54;
public static void openTeamGUI(Player player) {
FileConfiguration cfg = Main.getInstance().getConfig();
FileConfiguration data = DataManager.getData();
private static final int[] OVERVIEW_SLOTS_FALLBACK = {13, 22, 31, 40, 11, 15, 29, 33};
int size = cfg.getInt("gui.size", 54);
String title = Utils.color(cfg.getString("gui.title", "&8» &bTeam Übersicht"));
Inventory inv = Bukkit.createInventory(null, size, title);
public static final int NAV_PREV = 45;
public static final int NAV_PREV_PAGE = 46;
public static final int NAV_HOME = 49;
public static final int NAV_NEXT_PAGE = 52;
public static final int NAV_NEXT = 53;
// Hintergrund-Glas setzen
Material bgMat = Material.valueOf(cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE"));
ItemStack filler = new ItemStack(bgMat);
ItemMeta fm = filler.getItemMeta();
if (fm != null) {
fm.setDisplayName(" ");
filler.setItemMeta(fm);
}
for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, filler);
/** UUID → current page on the rank page they have open */
private static final Map<UUID, Integer> RANK_PAGE = new HashMap<>();
List<String> ranks = cfg.getStringList("ranks");
for (int i = 0; i < ranks.size() && i < rows.length; i++) {
// ─────────────────────────────────────────────────────────────────
// PUBLIC ENTRY POINTS
// ─────────────────────────────────────────────────────────────────
public static void openTeamGUI(Player player) { openOverview(player); }
// ─────────────────────────────────────────────────────────────────
// LAYER 1 OVERVIEW
// ─────────────────────────────────────────────────────────────────
public static void openOverview(Player player) {
FileConfiguration cfg = Main.getInstance().getConfig();
List<String> ranks = cfg.getStringList("ranks");
Inventory inv = Bukkit.createInventory(null, GUI_SIZE, Utils.color(getGuiTitle()));
fillBackground(inv, cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE"));
for (int i = 0; i < ranks.size(); i++) {
String rank = ranks.get(i);
List<String> members = data.getStringList("Team." + rank);
if (members == null) members = new ArrayList<>();
int slot = resolveOverviewSlot(cfg, rank, i);
if (slot < 0 || slot >= GUI_SIZE) continue;
inv.setItem(slot, createRankOverviewBlock(rank));
}
player.openInventory(inv);
}
int rowStart = rows[i] * 9;
int count = Math.min(members.size(), 9);
if (count == 0) {
ItemStack empty = createInfoItem("§7Kein/e " + rank, List.of("§7Keine Mitglieder"));
inv.setItem(rowStart + 4, empty);
continue;
private static int resolveOverviewSlot(FileConfiguration cfg, String rank, int index) {
if (cfg.isInt("rank-settings." + rank + ".slot"))
return cfg.getInt("rank-settings." + rank + ".slot");
if (index < OVERVIEW_SLOTS_FALLBACK.length)
return OVERVIEW_SLOTS_FALLBACK[index];
return -1;
}
private static ItemStack createRankOverviewBlock(String rank) {
FileConfiguration cfg = Main.getInstance().getConfig();
String displayRaw = cfg.getString("rank-settings." + rank + ".display", rank);
String skullTexture = cfg.getString("rank-settings." + rank + ".skull_texture", "");
int memberCount = DataManager.getData().getStringList("Team." + rank).size();
ItemStack item = skullTexture.isEmpty()
? new ItemStack(parseMaterial(cfg.getString("rank-settings." + rank + ".material", "STONE"), Material.STONE))
: buildCustomSkull(skullTexture);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(Utils.color(displayRaw));
List<String> lore = new ArrayList<>();
lore.add(Utils.color("&7Mitglieder&8: &e" + memberCount));
lore.add("");
lore.add(Utils.color("&7\u25ba &fKlicke zum \u00d6ffnen"));
meta.setLore(lore);
item.setItemMeta(meta);
}
return item;
}
// ─────────────────────────────────────────────────────────────────
// LAYER 2 RANK PAGE (with pagination)
// ─────────────────────────────────────────────────────────────────
public static void openRankPage(Player player, String rank) {
openRankPage(player, rank, 0);
}
public static void openRankPage(Player player, String rank, int page) {
FileConfiguration cfg = Main.getInstance().getConfig();
List<String> ranks = cfg.getStringList("ranks");
int rankIdx = ranks.indexOf(rank);
if (rankIdx < 0) return;
int rankCount = ranks.size();
// All members for this rank
List<String> allMembers = DataManager.getData().getStringList("Team." + rank);
// Determine page capacity
List<Integer> configuredSlots = cfg.getIntegerList("rank-settings." + rank + ".member_slots")
.stream().filter(s -> s >= 0 && s < 45).collect(Collectors.toList());
int capacity = configuredSlots.isEmpty()
? Math.min(cfg.getInt("gui.members_per_page", 40), 45)
: configuredSlots.size();
int totalPages = Math.max(1, (int) Math.ceil((double) allMembers.size() / capacity));
page = Math.max(0, Math.min(page, totalPages - 1));
RANK_PAGE.put(player.getUniqueId(), page);
// Slice for this page
int start = page * capacity;
int end = Math.min(start + capacity, allMembers.size());
List<String> members = allMembers.subList(start, end);
String glassStr = cfg.getString("rank-settings." + rank + ".glass",
cfg.getString("gui.background", "GRAY_STAINED_GLASS_PANE"));
Inventory inv = Bukkit.createInventory(null, GUI_SIZE, Utils.color(getRankPageTitle(rank)));
fillBackground(inv, glassStr);
// Dark footer row
ItemStack navFiller = createFiller(Material.BLACK_STAINED_GLASS_PANE);
for (int i = 45; i < 54; i++) inv.setItem(i, navFiller);
// ── Rank navigation (always visible) ────────────────────────
String prevRank = ranks.get((rankIdx - 1 + rankCount) % rankCount);
String prevDisplay = cfg.getString("rank-settings." + prevRank + ".display", prevRank);
inv.setItem(NAV_PREV, createNavItem(Material.ARROW,
Utils.color(Utils.replace(LangManager.get("nav_prev_label"), "%rank%", prevDisplay)),
List.of(Utils.color(LangManager.get("nav_prev_lore")))));
inv.setItem(NAV_HOME, createNavItem(Material.NETHER_STAR,
Utils.color(LangManager.get("nav_home_label")),
List.of(Utils.color(LangManager.get("nav_home_lore")))));
String nextRank = ranks.get((rankIdx + 1) % rankCount);
String nextDisplay = cfg.getString("rank-settings." + nextRank + ".display", nextRank);
inv.setItem(NAV_NEXT, createNavItem(Material.ARROW,
Utils.color(Utils.replace(LangManager.get("nav_next_label"), "%rank%", nextDisplay)),
List.of(Utils.color(LangManager.get("nav_next_lore")))));
// ── Page navigation (only if multiple pages) ─────────────────
if (totalPages > 1) {
if (page > 0) inv.setItem(NAV_PREV_PAGE, createNavItem(Material.SPECTRAL_ARROW,
Utils.color(LangManager.get("page_prev_label")),
List.of(Utils.color("&7Seite " + page + " / " + totalPages))));
// Page indicator in home slot lore (update lore only)
ItemStack homeItem = inv.getItem(NAV_HOME);
if (homeItem != null) {
ItemMeta hm = homeItem.getItemMeta();
if (hm != null) {
hm.setLore(List.of(
Utils.color(LangManager.get("nav_home_lore")),
Utils.color("&8Seite &7" + (page + 1) + " &8/ &7" + totalPages)
));
homeItem.setItemMeta(hm);
}
}
int startOffset = (9 - count) / 2;
for (int j = 0; j < count; j++) {
String name = members.get(j);
int slot = rowStart + startOffset + j;
if (slot >= 0 && slot < inv.getSize()) {
inv.setItem(slot, createPlayerHead(name, rank));
}
if (page + 1 < totalPages) inv.setItem(NAV_NEXT_PAGE, createNavItem(Material.SPECTRAL_ARROW,
Utils.color(LangManager.get("page_next_label")),
List.of(Utils.color("&7Seite " + (page + 2) + " / " + totalPages))));
}
// ── Member heads ──────────────────────────────────────────────
List<Integer> memberSlots = resolveMemberSlots(cfg, rank, members.size());
if (allMembers.isEmpty()) {
inv.setItem(22, createInfoItem("\u00a77Keine Mitglieder",
List.of("\u00a77Dieser Rang hat keine Mitglieder.")));
} else {
for (int i = 0; i < members.size() && i < memberSlots.size(); i++) {
int slot = memberSlots.get(i);
String memberName = members.get(i);
inv.setItem(slot, createPlayerHeadSync(memberName, rank));
final int finalSlot = slot;
SkinResolver.resolveAndUpdate(memberName, inv, finalSlot,
(uuid, updatedInv) -> {
if (!player.isOnline()) return;
if (!player.getOpenInventory().getTitle()
.equals(Utils.color(getRankPageTitle(rank)))) return;
updatedInv.setItem(finalSlot,
createPlayerHeadWithUUID(memberName, rank, uuid));
});
}
}
player.openInventory(inv);
}
private static ItemStack createPlayerHead(String name, String rank) {
FileConfiguration cfg = Main.getInstance().getConfig();
private static List<Integer> resolveMemberSlots(FileConfiguration cfg, String rank, int count) {
List<Integer> configured = cfg.getIntegerList("rank-settings." + rank + ".member_slots")
.stream().filter(s -> s >= 0 && s < 45).collect(Collectors.toList());
return (!configured.isEmpty()) ? configured : computeSlots(count);
}
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) {
OfflinePlayer off = Bukkit.getOfflinePlayer(name);
meta.setOwningPlayer(off);
// Rank aus config
String rankDisplay = cfg.getString("rank-settings." + rank + ".display", rank);
String rankPrefix = cfg.getString("rank-settings." + rank + ".prefix", "");
// Name + Prefix
String displayName = (rankPrefix == null ? "" : rankPrefix + " ") + "&b" + name;
meta.setDisplayName(Utils.color(displayName.trim()));
List<String> lore = new ArrayList<>();
// Rang
String rankLine = Utils.replace(LangManager.get("tooltip_rank"), "%rank%", rankDisplay);
lore.add(Utils.color(rankLine));
// Online-/Offline-Status aus config
String statusOnline = cfg.getString("status.online", "&a🟢 Online");
String statusOffline = cfg.getString("status.offline", "&c🔴 Offline");
boolean isOnline = off.isOnline();
String statusLine = isOnline ? statusOnline : statusOffline;
lore.add(Utils.color("&7Status: " + statusLine));
// Join-Datum, falls vorhanden
String iso = DataManager.getData().getString("JoinDates." + name, "");
if (iso != null && !iso.isEmpty()) {
String joinLine = Utils.replace(LangManager.get("tooltip_joined"), "%joindate%", Utils.prettifyIso(iso));
lore.add(Utils.color(joinLine));
private static List<Integer> computeSlots(int count) {
List<Integer> result = new ArrayList<>();
if (count == 0) return result;
final int MAX = 45, COLS = 9, ROWS = 5;
int total = Math.min(count, MAX);
int perRow = Math.min((int) Math.ceil((double) total / ROWS), COLS);
int placed = 0;
for (int row = 0; row < ROWS && placed < total; row++) {
int inRow = Math.min(perRow, total - placed);
int offset = (COLS - inRow) / 2;
for (int col = 0; col < inRow && placed < total; col++) {
result.add(row * COLS + offset + col);
placed++;
}
}
return result;
}
meta.setLore(lore);
skull.setItemMeta(meta);
// ─────────────────────────────────────────────────────────────────
// CLICK HANDLERS
// ─────────────────────────────────────────────────────────────────
public static void handleOverviewClick(Player player, int slot) {
FileConfiguration cfg = Main.getInstance().getConfig();
List<String> ranks = cfg.getStringList("ranks");
for (int i = 0; i < ranks.size(); i++) {
if (slot == resolveOverviewSlot(cfg, ranks.get(i), i)) {
openRankPage(player, ranks.get(i));
return;
}
}
}
public static void handleRankPageClick(Player player, int slot, String currentRank) {
List<String> ranks = Main.getInstance().getConfig().getStringList("ranks");
int idx = ranks.indexOf(currentRank);
int size = ranks.size();
if (idx < 0 || size == 0) return;
int page = RANK_PAGE.getOrDefault(player.getUniqueId(), 0);
switch (slot) {
case NAV_HOME -> openOverview(player);
case NAV_PREV -> openRankPage(player, ranks.get((idx - 1 + size) % size));
case NAV_NEXT -> openRankPage(player, ranks.get((idx + 1) % size));
case NAV_PREV_PAGE -> openRankPage(player, currentRank, page - 1);
case NAV_NEXT_PAGE -> openRankPage(player, currentRank, page + 1);
default -> {
// Member head click → message flow
String targetName = getMemberAtSlot(currentRank, slot, page);
if (targetName != null && !targetName.equalsIgnoreCase(player.getName())) {
player.closeInventory();
MessageManager.startInput(player, targetName);
}
}
}
}
// ─────────────────────────────────────────────────────────────────
// HEAD → MEMBER LOOKUP
// ─────────────────────────────────────────────────────────────────
/**
* Returns the member name at the given slot on a rank page (accounting for page offset),
* or null if the slot holds no member head.
*/
public static String getMemberAtSlot(String rank, int slot, int page) {
FileConfiguration cfg = Main.getInstance().getConfig();
List<String> allMembers = DataManager.getData().getStringList("Team." + rank);
List<Integer> configuredSlots = cfg.getIntegerList("rank-settings." + rank + ".member_slots")
.stream().filter(s -> s >= 0 && s < 45).collect(Collectors.toList());
int capacity = configuredSlots.isEmpty()
? Math.min(cfg.getInt("gui.members_per_page", 40), 45)
: configuredSlots.size();
int start = page * capacity;
int end = Math.min(start + capacity, allMembers.size());
List<String> pageMembers = allMembers.subList(start, end);
List<Integer> memberSlots = resolveMemberSlots(cfg, rank, pageMembers.size());
for (int i = 0; i < pageMembers.size() && i < memberSlots.size(); i++) {
if (memberSlots.get(i) == slot) return pageMembers.get(i);
}
return null;
}
// ─────────────────────────────────────────────────────────────────
// TITLE HELPERS
// ─────────────────────────────────────────────────────────────────
public static String getGuiTitle() {
return Main.getInstance().getConfig().getString("gui.title", "&8\u00bb &bTeam \u00dcbersicht");
}
public static String getRankPageTitle(String rank) {
FileConfiguration cfg = Main.getInstance().getConfig();
String display = cfg.getString("rank-settings." + rank + ".display", rank);
String prefix = cfg.getString("gui.rank_page_title", "&8\u00bb &bTeam &7| ");
return prefix + display;
}
public static boolean isOverviewTitle(String coloredTitle) {
return coloredTitle.equals(Utils.color(getGuiTitle()));
}
public static String extractRankFromTitle(String coloredTitle) {
for (String rank : Main.getInstance().getConfig().getStringList("ranks")) {
if (coloredTitle.equals(Utils.color(getRankPageTitle(rank)))) return rank;
}
return null;
}
// ─────────────────────────────────────────────────────────────────
// HEAD FACTORIES
// ─────────────────────────────────────────────────────────────────
private static ItemStack createPlayerHeadSync(String name, String rank) {
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta == null) return skull;
@SuppressWarnings("deprecation")
OfflinePlayer off = Bukkit.getOfflinePlayer(name);
meta.setOwningPlayer(off);
applyHeadMeta(meta, name, rank);
skull.setItemMeta(meta);
return skull;
}
private static ItemStack createPlayerHeadWithUUID(String name, String rank, UUID uuid) {
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta == null) return skull;
OfflinePlayer off = (uuid != null) ? Bukkit.getOfflinePlayer(uuid)
: Bukkit.getOfflinePlayer(name); //noinspection deprecation
meta.setOwningPlayer(off);
applyHeadMeta(meta, name, rank);
skull.setItemMeta(meta);
return skull;
}
private static void applyHeadMeta(SkullMeta meta, String name, String rank) {
FileConfiguration cfg = Main.getInstance().getConfig();
String rankPrefix = cfg.getString("rank-settings." + rank + ".prefix", "");
String displayName = rankPrefix.isEmpty() ? "&b" + name : rankPrefix + " &b" + name;
meta.setDisplayName(Utils.color(displayName.trim()));
List<String> lore = new ArrayList<>();
String rankDisplay = cfg.getString("rank-settings." + rank + ".display", rank);
lore.add(Utils.color(Utils.replace(LangManager.get("tooltip_rank"), "%rank%", rankDisplay)));
String statusOnline = cfg.getString("status.online", "&a\uD83D\uDFE2 Online");
String statusOffline = cfg.getString("status.offline", "&c\uD83D\uDD34 Offline");
@SuppressWarnings("deprecation")
boolean online = Bukkit.getOfflinePlayer(name).isOnline();
lore.add(Utils.color("&7Status&8: " + (online ? statusOnline : statusOffline)));
String iso = DataManager.getData().getString("JoinDates." + name, "");
if (iso != null && !iso.isEmpty())
lore.add(Utils.color(Utils.replace(LangManager.get("tooltip_joined"),
"%joindate%", Utils.prettifyIso(iso))));
lore.add("");
lore.add(Utils.color(LangManager.get("msg_head_hint")));
meta.setLore(lore);
}
// ─────────────────────────────────────────────────────────────────
// CUSTOM SKULL
// ─────────────────────────────────────────────────────────────────
private static ItemStack buildCustomSkull(String texture) {
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
if (texture == null || texture.isEmpty()) return skull;
try {
String url = resolveTextureUrl(texture);
if (url == null) {
Main.getInstance().getLogger().warning("[TeamGUI] Cannot resolve texture: " + texture);
return skull;
}
PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID(), "CustomSkull");
PlayerTextures textures = profile.getTextures();
textures.setSkin(new java.net.URL(url));
profile.setTextures(textures);
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) { meta.setOwnerProfile(profile); skull.setItemMeta(meta); }
} catch (Exception e) {
Main.getInstance().getLogger().warning("[TeamGUI] Skull error: " + e.getMessage());
}
return skull;
}
private static String resolveTextureUrl(String texture) {
if (texture == null || texture.isEmpty()) return null;
if (texture.startsWith("http://") || texture.startsWith("https://")) return texture;
if (texture.matches("[0-9a-fA-F]{64}")) return "http://textures.minecraft.net/texture/" + texture;
if (texture.startsWith("eyJ")) {
try {
String json = new String(Base64.getDecoder().decode(texture), StandardCharsets.UTF_8);
int s = json.indexOf("\"url\":\"");
if (s < 0) return null;
s += 7;
int e = json.indexOf('"', s);
return (e > s) ? json.substring(s, e) : null;
} catch (IllegalArgumentException ignored) {}
}
return null;
}
// ─────────────────────────────────────────────────────────────────
// BACKGROUND / GENERIC HELPERS
// ─────────────────────────────────────────────────────────────────
private static void fillBackground(Inventory inv, String matStr) {
ItemStack filler = createFiller(parseMaterial(matStr, Material.GRAY_STAINED_GLASS_PANE));
for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, filler);
}
private static ItemStack createFiller(Material mat) {
ItemStack item = new ItemStack(mat);
ItemMeta m = item.getItemMeta();
if (m != null) { m.setDisplayName(" "); item.setItemMeta(m); }
return item;
}
private static ItemStack createNavItem(Material mat, String name, List<String> lore) {
ItemStack item = new ItemStack(mat);
ItemMeta m = item.getItemMeta();
if (m != null) { m.setDisplayName(name); m.setLore(lore); item.setItemMeta(m); }
return item;
}
private static ItemStack createInfoItem(String name, List<String> lore) {
ItemStack item = new ItemStack(Material.PAPER);
ItemMeta m = item.getItemMeta();
ItemMeta m = item.getItemMeta();
if (m != null) {
m.setDisplayName(Utils.color(name));
m.setLore(lore.stream().map(Utils::color).toList());
@@ -121,7 +479,7 @@ public class TeamGUI {
return item;
}
public static String getGuiTitle() {
return Main.getInstance().getConfig().getString("gui.title", "&8» &bTeam Übersicht");
private static Material parseMaterial(String s, Material fallback) {
try { return Material.valueOf(s); } catch (Exception e) { return fallback; }
}
}
}