From ab037716c469b7db9b0dda3f6e0b2263704f600d Mon Sep 17 00:00:00 2001 From: Shiewk Date: Mon, 29 Jul 2024 13:22:00 +0200 Subject: [PATCH] Add search option in SMod menu with chat input --- .../de/shiewk/smoderation/SModeration.java | 2 + .../shiewk/smoderation/input/ChatInput.java | 70 +++++++++++++++ .../smoderation/input/ChatInputListener.java | 32 +++++++ .../smoderation/inventory/SModMenu.java | 85 ++++++++++++++----- .../smoderation/punishments/Punishment.java | 19 +++++ 5 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 src/main/java/de/shiewk/smoderation/input/ChatInput.java create mode 100644 src/main/java/de/shiewk/smoderation/input/ChatInputListener.java diff --git a/src/main/java/de/shiewk/smoderation/SModeration.java b/src/main/java/de/shiewk/smoderation/SModeration.java index b0caeb0..f30a550 100644 --- a/src/main/java/de/shiewk/smoderation/SModeration.java +++ b/src/main/java/de/shiewk/smoderation/SModeration.java @@ -4,6 +4,7 @@ import de.shiewk.smoderation.command.*; import de.shiewk.smoderation.event.CustomInventoryEvents; import de.shiewk.smoderation.event.EnderchestSeeEvents; import de.shiewk.smoderation.event.InvSeeEvents; +import de.shiewk.smoderation.input.ChatInputListener; import de.shiewk.smoderation.listener.PunishmentListener; import de.shiewk.smoderation.listener.VanishListener; import de.shiewk.smoderation.storage.PunishmentContainer; @@ -51,6 +52,7 @@ public final class SModeration extends JavaPlugin { getPluginManager().registerEvents(new InvSeeEvents(), this); getPluginManager().registerEvents(new EnderchestSeeEvents(), this); getPluginManager().registerEvents(new VanishListener(), this); + getPluginManager().registerEvents(new ChatInputListener(), this); registerCommand("mute", new MuteCommand()); registerCommand("ban", new BanCommand()); diff --git a/src/main/java/de/shiewk/smoderation/input/ChatInput.java b/src/main/java/de/shiewk/smoderation/input/ChatInput.java new file mode 100644 index 0000000..036bd6a --- /dev/null +++ b/src/main/java/de/shiewk/smoderation/input/ChatInput.java @@ -0,0 +1,70 @@ +package de.shiewk.smoderation.input; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.Title; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import static de.shiewk.smoderation.SModeration.CHAT_PREFIX; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; + +public class ChatInput { + private final Player player; + private final Component prompt; + private final Consumer action; + private int remainingTicks; + + private ChatInput(@NotNull Player player, @NotNull Component prompt, @NotNull Consumer action, int remainingSeconds){ + Objects.requireNonNull(action); + Objects.requireNonNull(prompt); + Objects.requireNonNull(player); + this.player = player; + this.prompt = prompt; + this.action = action; + this.remainingTicks = remainingSeconds * 20; + } + + static void tickAll() { + runningInputs.values().forEach(ChatInput::tick); + } + + void tick(){ + remainingTicks--; + if (remainingTicks <= 0){ + runningInputs.remove(player); + return; + } + if (remainingTicks % 20 == 0){ + player.showTitle(Title.title( + text().content(getRemainingTicks() / 20 + " seconds").color(GRAY).build(), + getPrompt(), + Title.Times.times(Duration.ZERO, Duration.ofSeconds(2), Duration.ZERO) + )); + } + } + + final static ConcurrentHashMap runningInputs = new ConcurrentHashMap<>(); + + public static void prompt(Player player, Consumer consumer, Component prompt, int timeSeconds){ + runningInputs.put(player, new ChatInput(player, prompt, consumer, timeSeconds)); + player.sendMessage(CHAT_PREFIX.append(prompt)); + } + + public Component getPrompt() { + return prompt; + } + + public Consumer getAction() { + return action; + } + + public int getRemainingTicks() { + return remainingTicks; + } +} diff --git a/src/main/java/de/shiewk/smoderation/input/ChatInputListener.java b/src/main/java/de/shiewk/smoderation/input/ChatInputListener.java new file mode 100644 index 0000000..5d48a60 --- /dev/null +++ b/src/main/java/de/shiewk/smoderation/input/ChatInputListener.java @@ -0,0 +1,32 @@ +package de.shiewk.smoderation.input; + +import com.destroystokyo.paper.event.server.ServerTickStartEvent; +import io.papermc.paper.event.player.AsyncChatEvent; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +import static de.shiewk.smoderation.input.ChatInput.runningInputs; + +public class ChatInputListener implements Listener { + + + @EventHandler + public void onAsyncChat(AsyncChatEvent event){ + final ChatInput input = runningInputs.remove(event.getPlayer()); + if (input != null){ + event.setCancelled(true); + input.getAction().accept(event.message()); + } + } + + @EventHandler public void onPlayerQuit(PlayerQuitEvent event){ + runningInputs.remove(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) public void onServerTickStart(ServerTickStartEvent event){ + ChatInput.tickAll(); + } + +} diff --git a/src/main/java/de/shiewk/smoderation/inventory/SModMenu.java b/src/main/java/de/shiewk/smoderation/inventory/SModMenu.java index 52aee58..de7b74f 100644 --- a/src/main/java/de/shiewk/smoderation/inventory/SModMenu.java +++ b/src/main/java/de/shiewk/smoderation/inventory/SModMenu.java @@ -1,11 +1,13 @@ package de.shiewk.smoderation.inventory; +import de.shiewk.smoderation.input.ChatInput; import de.shiewk.smoderation.punishments.Punishment; import de.shiewk.smoderation.punishments.PunishmentType; import de.shiewk.smoderation.storage.PunishmentContainer; import de.shiewk.smoderation.util.PlayerUtil; import de.shiewk.smoderation.util.TimeUtil; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import org.bukkit.Bukkit; @@ -15,6 +17,7 @@ import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; @@ -28,6 +31,7 @@ import java.util.List; import java.util.function.Predicate; import static de.shiewk.smoderation.SModeration.*; +import static net.kyori.adventure.text.Component.text; public class SModMenu extends PageableCustomInventory { @@ -70,13 +74,15 @@ public class SModMenu extends PageableCustomInventory { private List punishments; private ItemStack sortStack = null; private ItemStack filterStack = null; + private ItemStack searchStack = null; private int sort = 0; private int filter = 0; + private String searchQuery = null; public SModMenu(Player player, PunishmentContainer container) { this.player = player; this.container = container; - this.inventory = Bukkit.createInventory(this, 54, Component.text("SMod Menu")); + this.inventory = Bukkit.createInventory(this, 54, text("SMod Menu")); reload(); } @@ -89,7 +95,18 @@ public class SModMenu extends PageableCustomInventory { } private void reload(){ - this.punishments = container.copy().stream().filter(getFilter().filter).sorted(getSort().comparator).toList(); + this.punishments = container.copy().stream().filter(getFilter().filter).filter(p -> p.matchesSearchQuery(searchQuery)).sorted(getSort().comparator).toList(); + } + + public void promptSearchQuery(){ + Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, player::closeInventory); + ChatInput.prompt(player, component -> { + if (component instanceof TextComponent text){ + this.searchQuery = text.content(); + // chat event is async + Bukkit.getScheduler().scheduleSyncDelayedTask(PLUGIN, this::open); + } + }, text("Enter your search query in chat").color(SECONDARY_COLOR), 30); } @Override @@ -144,16 +161,16 @@ public class SModMenu extends PageableCustomInventory { final Filter filter = getFilter(); final ItemStack stack = new ItemStack(Filter.ICON); stack.editMeta(meta -> { - meta.displayName(applyFormatting(Component.text("Filter: " + filter.name).color(SECONDARY_COLOR))); + meta.displayName(applyFormatting(text("Filter: " + filter.name).color(PRIMARY_COLOR))); ArrayList lore = new ArrayList<>(); lore.add(Component.empty()); for (Filter value : Filter.values()) { final boolean selected = filter == value; - Component filterText = applyFormatting(Component.text((selected ? "\u00BB " : "") + value.name).color(selected ? PRIMARY_COLOR : INACTIVE_COLOR)); + Component filterText = applyFormatting(text((selected ? "\u00BB " : "") + value.name).color(selected ? SECONDARY_COLOR : INACTIVE_COLOR)); lore.add(filterText); } lore.add(Component.empty()); - lore.add(applyFormatting(Component.text("\u00BB Click to switch filter").color(NamedTextColor.GOLD))); + lore.add(applyFormatting(text("\u00BB Click to switch filter").color(NamedTextColor.GOLD))); meta.lore(lore); }); filterStack = stack; @@ -164,35 +181,54 @@ public class SModMenu extends PageableCustomInventory { final Sort sort = getSort(); final ItemStack stack = new ItemStack(Sort.ICON); stack.editMeta(meta -> { - meta.displayName(applyFormatting(Component.text("Sort by: " + sort.name).color(PRIMARY_COLOR))); + meta.displayName(applyFormatting(text("Sort by: " + sort.name).color(PRIMARY_COLOR))); ArrayList lore = new ArrayList<>(); lore.add(Component.empty()); for (Sort value : Sort.values()) { final boolean selected = sort == value; - Component sortText = applyFormatting(Component.text((selected ? "\u00BB " : "") + value.name).color(selected ? SECONDARY_COLOR : INACTIVE_COLOR)); + Component sortText = applyFormatting(text((selected ? "\u00BB " : "") + value.name).color(selected ? SECONDARY_COLOR : INACTIVE_COLOR)); lore.add(sortText); } lore.add(Component.empty()); - lore.add(applyFormatting(Component.text("\u00BB Click to switch sorting option").color(NamedTextColor.GOLD))); + lore.add(applyFormatting(text("\u00BB Click to switch sorting option").color(NamedTextColor.GOLD))); meta.lore(lore); }); sortStack = stack; return stack; } + private ItemStack createSearchItem(){ + final ItemStack stack = new ItemStack(Material.FLOWER_BANNER_PATTERN); + stack.editMeta(meta -> { + meta.addItemFlags(ItemFlag.HIDE_ITEM_SPECIFICS); + meta.displayName(applyFormatting(text("Search").color(PRIMARY_COLOR))); + final ArrayList lore = new ArrayList<>(List.of( + Component.empty(), + applyFormatting(text("Current search query: %s".formatted(searchQuery == null ? "None" : "\"" + searchQuery + "\"")).color(SECONDARY_COLOR)), + Component.empty(), + applyFormatting(text("\u00BB Click to enter new search query").color(NamedTextColor.GOLD)) + )); + if (searchQuery != null){ + lore.add(applyFormatting(text("\u00BB Right click to remove search query").color(NamedTextColor.GOLD))); + } + meta.lore(lore); + }); + return searchStack = stack; + } + private ItemStack createPunishmentItem(Punishment punishment){ ItemStack stack = new ItemStack(Material.PLAYER_HEAD); stack.editMeta(meta -> { if (meta instanceof SkullMeta skullMeta){ skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(punishment.to)); } - meta.displayName(applyFormatting(Component.text(punishment.type.name).color(NamedTextColor.RED).decorate(TextDecoration.BOLD))); + meta.displayName(applyFormatting(text(punishment.type.name).color(NamedTextColor.RED).decorate(TextDecoration.BOLD))); ArrayList lore = new ArrayList<>(); - lore.add(applyFormatting(Component.text("Player: ").color(SECONDARY_COLOR).append(Component.text(PlayerUtil.offlinePlayerName(punishment.to)).color(PRIMARY_COLOR)))); - lore.add(applyFormatting(Component.text("Punished by: ").color(SECONDARY_COLOR).append(Component.text(PlayerUtil.offlinePlayerName(punishment.by)).color(PRIMARY_COLOR)))); - lore.add(applyFormatting(Component.text("Timestamp: ").color(SECONDARY_COLOR).append(Component.text(TimeUtil.calendarTimestamp(punishment.time)).color(PRIMARY_COLOR)))); + lore.add(applyFormatting(text("Player: ").color(SECONDARY_COLOR).append(text(PlayerUtil.offlinePlayerName(punishment.to)).color(PRIMARY_COLOR)))); + lore.add(applyFormatting(text("Punished by: ").color(SECONDARY_COLOR).append(text(PlayerUtil.offlinePlayerName(punishment.by)).color(PRIMARY_COLOR)))); + lore.add(applyFormatting(text("Timestamp: ").color(SECONDARY_COLOR).append(text(TimeUtil.calendarTimestamp(punishment.time)).color(PRIMARY_COLOR)))); if (punishment.type != PunishmentType.KICK){ - lore.add(applyFormatting(Component.text("Duration: ").color(SECONDARY_COLOR).append(Component.text(TimeUtil.formatTimeLong(punishment.until - punishment.time)).color(PRIMARY_COLOR)))); + lore.add(applyFormatting(text("Duration: ").color(SECONDARY_COLOR).append(text(TimeUtil.formatTimeLong(punishment.until - punishment.time)).color(PRIMARY_COLOR)))); long remainingTime = punishment.until - System.currentTimeMillis(); final String expires; if (remainingTime > 0){ @@ -201,15 +237,15 @@ public class SModMenu extends PageableCustomInventory { remainingTime *= -1; expires = TimeUtil.formatTimeLong(remainingTime) + " ago"; } - lore.add(applyFormatting(Component.text("Expires: ").color(SECONDARY_COLOR).append(Component.text(expires).color(PRIMARY_COLOR)))); + lore.add(applyFormatting(text("Expires: ").color(SECONDARY_COLOR).append(text(expires).color(PRIMARY_COLOR)))); } - lore.add(applyFormatting(Component.text("Reason: ").color(SECONDARY_COLOR).append(Component.text(punishment.reason).color(PRIMARY_COLOR)))); + lore.add(applyFormatting(text("Reason: ").color(SECONDARY_COLOR).append(text(punishment.reason).color(PRIMARY_COLOR)))); if (punishment.wasUndone()){ - lore.add(applyFormatting(Component.text("Undone by: ").color(NamedTextColor.RED).append(Component.text(PlayerUtil.offlinePlayerName(punishment.undoneBy())).color(NamedTextColor.GOLD)))); + lore.add(applyFormatting(text("Undone by: ").color(NamedTextColor.RED).append(text(PlayerUtil.offlinePlayerName(punishment.undoneBy())).color(NamedTextColor.GOLD)))); } else if (punishment.isActive()) { if ((punishment.type == PunishmentType.BAN && player.hasPermission("smod.unban")) || (punishment.type == PunishmentType.MUTE && player.hasPermission("smod.unmute"))){ lore.add(Component.empty()); - lore.add(applyFormatting(Component.text("\u00BB Click to undo punishment").color(NamedTextColor.GOLD))); + lore.add(applyFormatting(text("\u00BB Click to undo punishment").color(NamedTextColor.GOLD))); } } meta.lore(lore); @@ -224,8 +260,9 @@ public class SModMenu extends PageableCustomInventory { } inventory.setItem(45, createPreviousPageStack()); inventory.setItem(53, createNextPageStack()); - inventory.setItem(50, createFilterItem()); - inventory.setItem(48, createSortItem()); + inventory.setItem(51, createFilterItem()); + inventory.setItem(49, createSortItem()); + inventory.setItem(47, createSearchItem()); for (int i = 0; i < 45; i++) { int ci = i + (getPage() * 45); @@ -252,6 +289,16 @@ public class SModMenu extends PageableCustomInventory { cycleFilter(event.isRightClick()); } else if (stack.equals(sortStack)){ cycleSort(event.isRightClick()); + } else if (stack.equals(searchStack)){ + if (event.isRightClick() && searchQuery != null){ + player.playSound(player, Sound.UI_BUTTON_CLICK, 1f, 0.8f); + searchQuery = null; + reload(); + refresh(); + } else { + player.playSound(player, Sound.UI_BUTTON_CLICK, 1f, 2f); + promptSearchQuery(); + } } final ItemMeta itemMeta = stack.getItemMeta(); if (itemMeta != null) { diff --git a/src/main/java/de/shiewk/smoderation/punishments/Punishment.java b/src/main/java/de/shiewk/smoderation/punishments/Punishment.java index 3f89896..8b086d7 100644 --- a/src/main/java/de/shiewk/smoderation/punishments/Punishment.java +++ b/src/main/java/de/shiewk/smoderation/punishments/Punishment.java @@ -239,4 +239,23 @@ public class Punishment { default -> throw new IllegalStateException("Unknown punishment type " + type); } } + + public boolean matchesSearchQuery(String searchQuery) { + if (searchQuery == null) return true; + searchQuery = searchQuery.toLowerCase(); + return reason.toLowerCase().contains(searchQuery) + || by.toString().equalsIgnoreCase(searchQuery) + || to.toString().equalsIgnoreCase(searchQuery) + || getPlayerName().toLowerCase().contains(searchQuery) + || getModeratorName().toLowerCase().contains(searchQuery); + + } + + private String getPlayerName() { + return PlayerUtil.offlinePlayerName(to); + } + + private String getModeratorName() { + return PlayerUtil.offlinePlayerName(by); + } }