commit f941d2f367568210183d643d179f52139a657634 Author: Silent Date: Mon Oct 19 17:34:36 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..686bc14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target/ +.idea/ + +dependency-reduced-pom.xml + +.classpath +.project +*.iml \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..724014e --- /dev/null +++ b/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + tsp.headdb + HeadDB + 1.0 + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + jar + + HeadDB + Head Database + + + + papermc + https://papermc.io/repo/repository/maven-public/ + + + mojang-repo + https://libraries.minecraft.net/ + + + + + + com.destroystokyo.paper + paper-api + 1.16.3-R0.1-SNAPSHOT + provided + + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + com.mojang + authlib + 1.5.21 + provided + + + + \ No newline at end of file diff --git a/src/main/java/tsp/headdb/HeadDB.java b/src/main/java/tsp/headdb/HeadDB.java new file mode 100644 index 0000000..5648473 --- /dev/null +++ b/src/main/java/tsp/headdb/HeadDB.java @@ -0,0 +1,50 @@ +package tsp.headdb; + +import org.bukkit.plugin.java.JavaPlugin; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.command.Command_headdb; +import tsp.headdb.database.HeadDatabase; +import tsp.headdb.listener.PagedPaneListener; +import tsp.headdb.listener.MenuListener; +import tsp.headdb.util.Config; +import tsp.headdb.util.Log; +import tsp.headdb.util.Metrics; +import tsp.headdb.util.Utils; + +public class HeadDB extends JavaPlugin { + + private static HeadDB instance; + private static Config config; + + @Override + public void onEnable() { + instance = this; + Log.info("Loading HeadDB - " + getDescription().getVersion()); + saveDefaultConfig(); + config = new Config("plugins/HeadDB/config.yml"); + + Log.debug("Starting metrics..."); + new Metrics(this, Utils.METRICS_ID); + + Log.debug("Registering listeners..."); + new PagedPaneListener(this); + new MenuListener(this); + + Log.debug("Registering commands..."); + getCommand("headdb").setExecutor(new Command_headdb()); + + Log.debug("Initializing Database..."); + HeadDatabase.update(); + + Log.info("Done!"); + } + + public static Config getCfg() { + return config; + } + + public static HeadDB getInstance() { + return instance; + } + +} diff --git a/src/main/java/tsp/headdb/api/Head.java b/src/main/java/tsp/headdb/api/Head.java new file mode 100644 index 0000000..98a128c --- /dev/null +++ b/src/main/java/tsp/headdb/api/Head.java @@ -0,0 +1,139 @@ +package tsp.headdb.api; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import org.apache.commons.lang.Validate; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import tsp.headdb.database.Category; +import tsp.headdb.util.Log; +import tsp.headdb.util.Utils; +import tsp.headdb.util.XMaterial; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.UUID; + +public class Head { + + private String name; + private UUID uuid; + private String value; + private Category category; + private int id; + + public ItemStack getItemStack() { + Validate.notNull(name, "name must not be null!"); + Validate.notNull(uuid, "uuid must not be null!"); + Validate.notNull(value, "value must not be null!"); + Validate.notNull(category, "category must not be null!"); + + ItemStack item = XMaterial.PLAYER_HEAD.parseItem(); + if (item != null) { + SkullMeta meta = (SkullMeta) item.getItemMeta(); + meta.setDisplayName(Utils.colorize(category.getColor() + name)); + // set skull owner + GameProfile profile = new GameProfile(uuid, name); + profile.getProperties().put("textures", new Property("textures", value)); + Field profileField; + try { + profileField = meta.getClass().getDeclaredField("profile"); + profileField.setAccessible(true); + profileField.set(meta, profile); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e1) { + Log.error("Could not set skull owner for " + uuid.toString() + " | Stack Trace:"); + e1.printStackTrace(); + } + meta.setLore(Collections.singletonList(Utils.colorize("&cID: " + id))); + item.setItemMeta(meta); + } + + return item; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public UUID getUUID() { + return uuid; + } + + public void setUUID(UUID uuid) { + this.uuid = uuid; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public static class Builder { + + private String name; + private UUID uuid; + private String value; + private Category category; + private int id; + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withUUID(UUID uuid) { + this.uuid = uuid; + return this; + } + + public Builder withValue(String value) { + this.value = value; + return this; + } + + public Builder withCategory(Category category) { + this.category = category; + return this; + } + + public Builder withId(int id) { + this.id = id; + return this; + } + + public Head build() { + Head head = new Head(); + head.setName(name); + head.setUUID(uuid); + head.setValue(value); + head.setCategory(category); + head.setId(id); + return head; + } + + } + +} diff --git a/src/main/java/tsp/headdb/api/HeadAPI.java b/src/main/java/tsp/headdb/api/HeadAPI.java new file mode 100644 index 0000000..c0f1860 --- /dev/null +++ b/src/main/java/tsp/headdb/api/HeadAPI.java @@ -0,0 +1,57 @@ +package tsp.headdb.api; + +import org.bukkit.entity.Player; +import tsp.headdb.database.Category; +import tsp.headdb.database.HeadDatabase; +import tsp.headdb.inventory.InventoryUtils; + +import java.util.List; +import java.util.UUID; + +public class HeadAPI { + + public static void openDatabase(Player player) { + InventoryUtils.openDatabase(player); + } + + public static void openDatabase(Player player, Category category) { + InventoryUtils.openCategoryDatabase(player, category); + } + + public static void openDatabase(Player player, String search) { + InventoryUtils.openSearchDatabase(player, search); + } + + public static Head getHeadByID(int id) { + return HeadDatabase.getHeadByID(id); + } + + public static Head getHeadByUUID(UUID uuid) { + return HeadDatabase.getHeadByUUID(uuid); + } + + public static List getHeadsByName(String name) { + return HeadDatabase.getHeadsByName(name); + } + + public static List getHeadsByName(Category category, String name) { + return HeadDatabase.getHeadsByName(category, name); + } + + public static Head getHeadByValue(String value) { + return HeadDatabase.getHeadByValue(value); + } + + public static List getHeads(Category category) { + return HeadDatabase.getHeads(category); + } + + public static List getHeads() { + return HeadDatabase.getHeads(); + } + + public static void updateDatabase() { + HeadDatabase.update(); + } + +} diff --git a/src/main/java/tsp/headdb/command/Command_headdb.java b/src/main/java/tsp/headdb/command/Command_headdb.java new file mode 100644 index 0000000..f5ec943 --- /dev/null +++ b/src/main/java/tsp/headdb/command/Command_headdb.java @@ -0,0 +1,116 @@ +package tsp.headdb.command; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import tsp.headdb.HeadDB; +import tsp.headdb.api.Head; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.inventory.InventoryUtils; +import tsp.headdb.util.Utils; + +public class Command_headdb implements CommandExecutor { + + @Override + public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { + if (args.length == 0) { + if (!sender.hasPermission("headdb.open")) { + Utils.sendMessage(sender, "&cNo permission!"); + return true; + } + if (!(sender instanceof Player)) { + Utils.sendMessage(sender, "&cOnly players may open the database."); + return true; + } + Player player = (Player) sender; + + Utils.sendMessage(player, "Opening &cHead Database"); + InventoryUtils.openDatabase(player); + return true; + } + String sub = args[0]; + + if (sub.equalsIgnoreCase("info") || sub.equalsIgnoreCase("i")) { + Utils.sendMessage(sender, "Running &cHeadDB v" + HeadDB.getInstance().getDescription().getVersion()); + Utils.sendMessage(sender, "Created by &c" + HeadDB.getInstance().getDescription().getAuthors()); + return true; + } + + if (sub.equalsIgnoreCase("search") || sub.equalsIgnoreCase("s")) { + if (!sender.hasPermission("headdb.search")) { + Utils.sendMessage(sender, "&cNo permission!"); + return true; + } + if (args.length < 2) { + Utils.sendMessage(sender, "&c/hdb search "); + return true; + } + if (!(sender instanceof Player)) { + Utils.sendMessage(sender, "&cOnly players may open the database."); + return true; + } + Player player = (Player) sender; + + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < args.length; i++) { + builder.append(args[i]).append(" "); + } + String name = builder.toString(); + Utils.sendMessage(sender, "Searching for &e" + name); + InventoryUtils.openSearchDatabase(player, name); + return true; + } + + if (sub.equalsIgnoreCase("give") || sub.equalsIgnoreCase("g")) { + if (!sender.hasPermission("headdb.give")) { + Utils.sendMessage(sender, "&cNo permission!"); + return true; + } + if (args.length < 3) { + Utils.sendMessage(sender, "&c/hdb give &6[amount]"); + return true; + } + try { + int id = Integer.parseInt(args[1]); + Player target = Bukkit.getPlayer(args[2]); + if (target == null) { + Utils.sendMessage(sender, "&cPlayer is not online!"); + return true; + } + + int amount = 1; + if (args.length > 3) { + amount = Integer.parseInt(args[3]); + } + + Head head = HeadAPI.getHeadByID(id); + ItemStack item = head.getItemStack(); + if (item == null) { + Utils.sendMessage(sender, "&cCould not find head with id &e" + id); + return true; + } + item.setAmount(amount); + target.getInventory().addItem(item); + Utils.sendMessage(sender, "Given &c" + target.getName() + " &ex" + amount + " " + head.getName()); + return true; + } catch (NumberFormatException nfe) { + Utils.sendMessage(sender, "&cID/Amount must be a number!"); + return true; + } + } + + Utils.sendMessage(sender, " "); + Utils.sendMessage(sender, "&c&lHeadDB &c- &5Commands"); + Utils.sendMessage(sender, "&7&oParameters:&c command &9(aliases) &7- Description"); + Utils.sendMessage(sender, " > &c/hdb &7- Opens the database"); + Utils.sendMessage(sender, " > &c/hdb info &9(i) &7- Plugin Information"); + Utils.sendMessage(sender, " > &c/hdb search &9(s) &c &7- Search for heads matching a name"); + Utils.sendMessage(sender, " > &c/hdb give &9(g) &c &6[amount] &7- Give player a head"); + Utils.sendMessage(sender, " "); + return true; + } + +} diff --git a/src/main/java/tsp/headdb/database/Category.java b/src/main/java/tsp/headdb/database/Category.java new file mode 100644 index 0000000..969bec9 --- /dev/null +++ b/src/main/java/tsp/headdb/database/Category.java @@ -0,0 +1,66 @@ +package tsp.headdb.database; + +import org.bukkit.ChatColor; +import org.bukkit.inventory.ItemStack; +import tsp.headdb.api.Head; +import tsp.headdb.api.HeadAPI; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public enum Category { + + ALPHABET("alphabet", ChatColor.YELLOW), + ANIMALS("animals", ChatColor.DARK_AQUA), + BLOCKS("blocks", ChatColor.DARK_GRAY), + DECORATION("decoration", ChatColor.LIGHT_PURPLE), + FOOD_DRINKS("food-drinks", ChatColor.GOLD), + HUMANS("humans", ChatColor.DARK_BLUE), + HUMANOID("humanoid", ChatColor.AQUA), + MISCELLANEOUS("miscellaneous", ChatColor.DARK_GREEN), + MONSTERS("monsters", ChatColor.RED), + PLANTS("plants", ChatColor.GREEN); + + private final String name; + private final ChatColor color; + private final Map item = new HashMap<>(); + + Category(String name, ChatColor color) { + this.name = name; + this.color = color; + } + + public String getName() { + return name; + } + + public ChatColor getColor() { + return color; + } + + public ItemStack getItem() { + if (item.containsKey(this)) { + return item.get(this).getItemStack(); + } + + item.put(this, HeadAPI.getHeads(this).get(0)); + return getItem(); + } + + public static Category getByName(String name) { + for (Category category : Category.values()) { + if (category.getName().equals(name)) { + return category; + } + } + + return null; + } + + public static List getCategories() { + return Arrays.asList(Category.values()); + } + +} diff --git a/src/main/java/tsp/headdb/database/HeadDatabase.java b/src/main/java/tsp/headdb/database/HeadDatabase.java new file mode 100644 index 0000000..ba5eae1 --- /dev/null +++ b/src/main/java/tsp/headdb/database/HeadDatabase.java @@ -0,0 +1,167 @@ +package tsp.headdb.database; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import tsp.headdb.HeadDB; +import tsp.headdb.api.Head; +import tsp.headdb.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class HeadDatabase { + + private static final Map> HEADS = new HashMap<>(); + private static final String URL = "https://minecraft-heads.com/scripts/api.php?cat="; + private static long updated; + + public static Head getHeadByValue(String value) { + List heads = getHeads(); + for (Head head : heads) { + if (head.getValue().equals(value)) { + return head; + } + } + + return null; + } + + public static Head getHeadByID(int id) { + List heads = getHeads(); + for (Head head : heads) { + if (head.getId() == id) { + return head; + } + } + + return null; + } + + public static Head getHeadByUUID(UUID uuid) { + List heads = getHeads(); + for (Head head : heads) { + if (head.getUUID().equals(uuid)) { + return head; + } + } + + return null; + } + + public static List getHeadsByName(Category category, String name) { + List result = new ArrayList<>(); + List heads = getHeads(category); + for (Head head : heads) { + if (head.getName().toLowerCase().contains(name.toLowerCase())) { + result.add(head); + } + } + + return result; + } + + public static List getHeadsByName(String name) { + List result = new ArrayList<>(); + List heads = getHeads(); + for (Head head : heads) { + if (head.getName().toLowerCase().contains(name.toLowerCase())) { + result.add(head); + } + } + + return result; + } + + public static List getHeads(Category category) { + return HEADS.get(category); + } + + public static List getHeads() { + if (!HEADS.isEmpty() && !isLastUpdateOld()) { + List heads = new ArrayList<>(); + for (Category category : HEADS.keySet()) { + heads.addAll(HEADS.get(category)); + } + return heads; + } + + update(); + return getHeads(); + } + + public static Map> getHeadsNoCache() { + Map> result = new HashMap<>(); + List categories = Category.getCategories(); + + for (Category category : categories) { + Log.debug("Caching heads from: " + category.getName()); + List heads = new ArrayList<>(); + try { + String line; + StringBuilder response = new StringBuilder(); + + URLConnection connection = new URL(URL + category.getName()).openConnection(); + connection.setConnectTimeout(5000); + connection.setRequestProperty("User-Agent", "HeadDB"); + try (BufferedReader in = new BufferedReader( + new InputStreamReader( + connection.getInputStream()))) { + while ((line = in.readLine()) != null) { + response.append(line); + } + } + JSONParser parser = new JSONParser(); + JSONArray array = (JSONArray) parser.parse(response.toString()); + int id = 1; + for (Object o : array) { + JSONObject obj = (JSONObject) o; + Head head = new Head.Builder() + .withName(obj.get("name").toString()) + .withUUID(UUID.fromString(obj.get("uuid").toString())) + .withValue(obj.get("value").toString()) + .withCategory(category) + .withId(id) + .build(); + + id++; + heads.add(head); + } + } catch (ParseException | IOException e) { + Log.error("Failed to fetch heads (no-cache) | Stack Trace:"); + e.printStackTrace(); + } + + updated = System.nanoTime(); + result.put(category, heads); + } + + return result; + } + + public static void update() { + Map> heads = getHeadsNoCache(); + HEADS.clear(); + for (Map.Entry> entry : heads.entrySet()) { + HEADS.put(entry.getKey(), entry.getValue()); + } + } + + public static long getLastUpdate() { + long now = System.nanoTime(); + long elapsed = now - updated; + return TimeUnit.NANOSECONDS.toSeconds(elapsed); + } + + public static boolean isLastUpdateOld() { + if (HeadDB.getCfg() == null && getLastUpdate() >= 3600) return true; + return getLastUpdate() >= HeadDB.getCfg().getLong("refresh"); + } + +} diff --git a/src/main/java/tsp/headdb/inventory/Button.java b/src/main/java/tsp/headdb/inventory/Button.java new file mode 100644 index 0000000..c8f48cb --- /dev/null +++ b/src/main/java/tsp/headdb/inventory/Button.java @@ -0,0 +1,81 @@ +package tsp.headdb.inventory; + +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * A button + */ +public class Button { + + private static int counter; + private final int ID = counter++; + + private ItemStack itemStack; + private Consumer action; + + /** + * @param itemStack The Item + */ + @SuppressWarnings("unused") + public Button(ItemStack itemStack) { + this(itemStack, event -> { + }); + } + + /** + * @param itemStack The Item + * @param action The action + */ + public Button(ItemStack itemStack, Consumer action) { + this.itemStack = itemStack; + this.action = action; + } + + + + /** + * @return The icon + */ + @SuppressWarnings("WeakerAccess") + public ItemStack getItemStack() { + return itemStack; + } + + /** + * @param action The new action + */ + @SuppressWarnings("unused") + public void setAction(Consumer action) { + this.action = action; + } + + /** + * @param event The event that triggered it + */ + @SuppressWarnings("WeakerAccess") + public void onClick(InventoryClickEvent event) { + action.accept(event); + } + + // We do not want equals collisions. The default hashcode would not fulfil this contract. + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Button)) { + return false; + } + Button button = (Button) o; + return ID == button.ID; + } + + @Override + public int hashCode() { + return Objects.hash(ID); + } +} diff --git a/src/main/java/tsp/headdb/inventory/InventoryUtils.java b/src/main/java/tsp/headdb/inventory/InventoryUtils.java new file mode 100644 index 0000000..aaa0225 --- /dev/null +++ b/src/main/java/tsp/headdb/inventory/InventoryUtils.java @@ -0,0 +1,98 @@ +package tsp.headdb.inventory; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import tsp.headdb.api.Head; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.database.Category; +import tsp.headdb.util.Utils; +import tsp.headdb.util.XMaterial; + +import java.util.ArrayList; +import java.util.List; + +public class InventoryUtils { + + public static void openSearchDatabase(Player player, String search) { + PagedPane pane = new PagedPane(4, 6, Utils.colorize("&c&lHeadDB - &eSearch: " + search)); + + List heads = HeadAPI.getHeadsByName(search); + for (Head head : heads) { + pane.addButton(new Button(head.getItemStack(), e -> { + if (e.getClick() == ClickType.SHIFT_LEFT) { + ItemStack item = head.getItemStack(); + item.setAmount(64); + player.getInventory().addItem(item); + return; + } + player.getInventory().addItem(head.getItemStack()); + })); + } + + pane.open(player); + } + + public static void openCategoryDatabase(Player player, Category category) { + PagedPane pane = new PagedPane(4, 6, Utils.colorize("&c&lHeadDB - &e" + category.getName())); + + List heads = HeadAPI.getHeads(category); + for (Head head : heads) { + pane.addButton(new Button(head.getItemStack(), e -> { + if (e.getClick() == ClickType.SHIFT_LEFT) { + ItemStack item = head.getItemStack(); + item.setAmount(64); + player.getInventory().addItem(item); + return; + } + player.getInventory().addItem(head.getItemStack()); + })); + } + + pane.open(player); + } + + public static void openDatabase(Player player) { + Inventory inventory = Bukkit.createInventory(null, 54, Utils.colorize("&c&lHeadDB")); + + fillBorder(inventory, XMaterial.BLACK_STAINED_GLASS_PANE.parseItem()); + for (Category category : Category.getCategories()) { + ItemStack item = category.getItem(); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(Utils.colorize(category.getColor() + "&l" + category.getName().toUpperCase())); + List lore = new ArrayList<>(); + lore.add(Utils.colorize("&e" + HeadAPI.getHeads(category).size() + " heads")); + meta.setLore(lore); + item.setItemMeta(meta); + inventory.addItem(item); + } + + player.openInventory(inventory); + } + + public static void fillBorder(Inventory inv, ItemStack item) { + int size = inv.getSize(); + int rows = (size + 1) / 9; + + // Fill top + for (int i = 0; i < 9; i++) { + inv.setItem(i, item); + } + + // Fill bottom + for (int i = size - 9; i < size; i++) { + inv.setItem(i, item); + } + + // Fill sides + for (int i = 2; i <= rows - 1; i++) { + int[] slots = new int[]{i * 9 - 1, (i - 1) * 9}; + inv.setItem(slots[0], item); + inv.setItem(slots[1], item); + } + } + +} diff --git a/src/main/java/tsp/headdb/inventory/PagedPane.java b/src/main/java/tsp/headdb/inventory/PagedPane.java new file mode 100644 index 0000000..33954cd --- /dev/null +++ b/src/main/java/tsp/headdb/inventory/PagedPane.java @@ -0,0 +1,373 @@ +package tsp.headdb.inventory; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.util.Utils; +import tsp.headdb.util.XMaterial; + +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +/** + * A paged pane. Credits @ I Al Ianstaan + */ +public class PagedPane implements InventoryHolder { + + private Inventory inventory; + + private SortedMap pages = new TreeMap<>(); + private int currentIndex; + private int pageSize; + + @SuppressWarnings("WeakerAccess") + protected Button controlBack; + @SuppressWarnings("WeakerAccess") + protected Button controlNext; + + /** + * @param pageSize The page size. inventory rows - 2 + */ + public PagedPane(int pageSize, int rows, String title) { + Objects.requireNonNull(title, "title can not be null!"); + if (rows > 6) { + throw new IllegalArgumentException("Rows must be <= 6, got " + rows); + } + if (pageSize > 6) { + throw new IllegalArgumentException("Page size must be <= 6, got" + pageSize); + } + + this.pageSize = pageSize; + inventory = Bukkit.createInventory(this, rows * 9, color(title)); + + pages.put(0, new Page(pageSize)); + } + + /** + * @param button The button to add + */ + public void addButton(Button button) { + for (Entry entry : pages.entrySet()) { + if (entry.getValue().addButton(button)) { + if (entry.getKey() == currentIndex) { + reRender(); + } + return; + } + } + Page page = new Page(pageSize); + page.addButton(button); + pages.put(pages.lastKey() + 1, page); + + reRender(); + } + + /** + * @param button The Button to remove + */ + @SuppressWarnings("unused") + public void removeButton(Button button) { + for (Iterator> iterator = pages.entrySet().iterator(); iterator.hasNext(); ) { + Entry entry = iterator.next(); + if (entry.getValue().removeButton(button)) { + + // we may need to delete the page + if (entry.getValue().isEmpty()) { + // we have more than one page, so delete it + if (pages.size() > 1) { + iterator.remove(); + } + // the currentIndex now points to a page that does not exist. Correct it. + if (currentIndex >= pages.size()) { + currentIndex--; + } + } + // if we modified the current one, re-render + // if we deleted the current page, re-render too + if (entry.getKey() >= currentIndex) { + reRender(); + } + return; + } + } + } + + /** + * @return The amount of pages + */ + @SuppressWarnings("WeakerAccess") + public int getPageAmount() { + return pages.size(); + } + + /** + * @return The number of the current page (1 based) + */ + @SuppressWarnings("WeakerAccess") + public int getCurrentPage() { + return currentIndex + 1; + } + + /** + * @param index The index of the new page + */ + @SuppressWarnings("WeakerAccess") + public void selectPage(int index) { + if (index < 0 || index >= getPageAmount()) { + throw new IllegalArgumentException( + "Index out of bounds s: " + index + " [" + 0 + " " + getPageAmount() + ")" + ); + } + if (index == currentIndex) { + return; + } + + currentIndex = index; + reRender(); + } + + /** + * Renders the inventory again + */ + @SuppressWarnings("WeakerAccess") + public void reRender() { + inventory.clear(); + pages.get(currentIndex).render(inventory); + + controlBack = null; + controlNext = null; + createControls(inventory); + } + + /** + * @param event The {@link InventoryClickEvent} + */ + @SuppressWarnings("WeakerAccess") + public void onClick(InventoryClickEvent event) { + event.setCancelled(true); + + // back item + if (event.getSlot() == inventory.getSize() - 8) { + if (controlBack != null) { + controlBack.onClick(event); + } + return; + } + // next item + else if (event.getSlot() == inventory.getSize() - 2) { + if (controlNext != null) { + controlNext.onClick(event); + } + return; + } + + pages.get(currentIndex).handleClick(event); + + } + + /** + * Get the object's inventory. + * + * @return The inventory. + */ + @Override + public Inventory getInventory() { + return inventory; + } + + /** + * Creates the controls + * + * @param inventory The inventory + */ + @SuppressWarnings("WeakerAccess") + protected void createControls(Inventory inventory) { + // create separator + fillRow( + inventory.getSize() / 9 - 2, + XMaterial.BLACK_STAINED_GLASS_PANE.parseItem(), + inventory + ); + + if (getCurrentPage() > 1) { + String name = String.format( + Locale.ROOT, + "&3&lPage &a&l%d &7/ &c&l%d", + getCurrentPage() - 1, getPageAmount() + ); + String lore = String.format( + Locale.ROOT, + "&7Previous: &c%d", + getCurrentPage() - 1 + ); + ItemStack itemStack = setMeta(HeadAPI.getHeadByValue("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODY1MmUyYjkzNmNhODAyNmJkMjg2NTFkN2M5ZjI4MTlkMmU5MjM2OTc3MzRkMThkZmRiMTM1NTBmOGZkYWQ1ZiJ9fX0=").getItemStack(), name, lore); + controlBack = new Button(itemStack, event -> selectPage(currentIndex - 1)); + inventory.setItem(inventory.getSize() - 8, itemStack); + } + + if (getCurrentPage() < getPageAmount()) { + String name = String.format( + Locale.ROOT, + "&3&lPage &a&l%d &7/ &c&l%d", + getCurrentPage() + 1, getPageAmount() + ); + String lore = String.format( + Locale.ROOT, + "&7Next: &c%d", + getCurrentPage() + 1 + ); + ItemStack itemStack = setMeta(HeadAPI.getHeadByValue("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmEzYjhmNjgxZGFhZDhiZjQzNmNhZThkYTNmZTgxMzFmNjJhMTYyYWI4MWFmNjM5YzNlMDY0NGFhNmFiYWMyZiJ9fX0=").getItemStack(), name, lore); + controlNext = new Button(itemStack, event -> selectPage(getCurrentPage())); + inventory.setItem(inventory.getSize() - 2, itemStack); + } + + { + String name = String.format( + Locale.ROOT, + "&3&lPage &a&l%d &7/ &c&l%d", + getCurrentPage(), getPageAmount() + ); + String lore = String.format( + Locale.ROOT, + "&7Current: &a%d", + getCurrentPage() + ); + ItemStack itemStack = setMeta(HeadAPI.getHeadByValue("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvY2Q5MWY1MTI2NmVkZGM2MjA3ZjEyYWU4ZDdhNDljNWRiMDQxNWFkYTA0ZGFiOTJiYjc2ODZhZmRiMTdmNGQ0ZSJ9fX0=").getItemStack(), name, lore); + inventory.setItem(inventory.getSize() - 5, itemStack); + } + } + + private void fillRow(int rowIndex, ItemStack itemStack, Inventory inventory) { + int yMod = rowIndex * 9; + for (int i = 0; i < 9; i++) { + int slot = yMod + i; + inventory.setItem(slot, itemStack); + } + } + + protected ItemStack setMeta(ItemStack itemStack, String name, String... lore) { + ItemMeta meta = itemStack.getItemMeta(); + meta.setDisplayName(Utils.colorize(name)); + meta.setLore(Arrays.stream(lore).map(this::color).collect(Collectors.toList())); + itemStack.setItemMeta(meta); + return itemStack; + } + + @SuppressWarnings("WeakerAccess") + @Deprecated + protected ItemStack getItemStack(Material type, int durability, String name, String... lore) { + ItemStack itemStack = new ItemStack(type, 1, (short) durability); + + ItemMeta itemMeta = itemStack.getItemMeta(); + + if (name != null) { + itemMeta.setDisplayName(color(name)); + } + if (lore != null && lore.length != 0) { + itemMeta.setLore(Arrays.stream(lore).map(this::color).collect(Collectors.toList())); + } + itemStack.setItemMeta(itemMeta); + + return itemStack; + } + + @SuppressWarnings("WeakerAccess") + protected String color(String input) { + return ChatColor.translateAlternateColorCodes('&', input); + } + + /** + * @param player The {@link Player} to open it for + */ + public void open(Player player) { + reRender(); + player.openInventory(getInventory()); + } + + + private static class Page { + private List