diff --git a/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java b/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java index 8db43ce..e264209 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java @@ -39,12 +39,16 @@ import org.bukkit.inventory.ItemFactory; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.logging.Level; @@ -61,6 +65,8 @@ public class BooksWithoutBorders extends JavaPlugin { private static ItemFactory itemFactory; private static Map> playerBooksList; private static List publicBooksList; + private static Map publicLetterIndex; + private static Map> playerLetterIndex; private static BooksWithoutBorders booksWithoutBorders; private static ConsoleCommandSender consoleSender; @@ -88,6 +94,7 @@ public class BooksWithoutBorders extends JavaPlugin { if (!playerBooksList.containsKey(playerUUID)) { List newFiles = BookFileHelper.listFiles(sender, false); playerBooksList.put(playerUUID, newFiles); + playerLetterIndex.put(playerUUID, BookFileHelper.populateLetterIndices(newFiles)); } return playerBooksList.get(playerUUID); } else { @@ -95,6 +102,22 @@ public class BooksWithoutBorders extends JavaPlugin { } } + /** + * Gets the letter index map for public books, or a specific player's books + * + * @param playerIndex

The player to get the index for, or null for the public index

+ * @return

An index mapping between a character and the first index containing that character

+ */ + @NotNull + public static Map getLetterIndex(@Nullable UUID playerIndex) { + if (playerIndex == null) { + return publicLetterIndex; + } else { + Map letterIndex = playerLetterIndex.get(playerIndex); + return Objects.requireNonNullElseGet(letterIndex, HashMap::new); + } + } + /** * Updates available books * @@ -103,10 +126,13 @@ public class BooksWithoutBorders extends JavaPlugin { */ public static void updateBooks(CommandSender sender, boolean updatePublic) { List newFiles = BookFileHelper.listFiles(sender, updatePublic); + newFiles.sort(Comparator.naturalOrder()); if (updatePublic) { publicBooksList = newFiles; + publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles); } else if (sender instanceof Player player) { playerBooksList.put(player.getUniqueId(), newFiles); + playerLetterIndex.put(player.getUniqueId(), BookFileHelper.populateLetterIndices(newFiles)); } } @@ -123,8 +149,10 @@ public class BooksWithoutBorders extends JavaPlugin { booksWithoutBorders = this; consoleSender = this.getServer().getConsoleSender(); playerBooksList = new HashMap<>(); + playerLetterIndex = new HashMap<>(); BooksWithoutBordersConfig.initialize(this); publicBooksList = BookFileHelper.listFiles(consoleSender, true); + publicLetterIndex = BookFileHelper.populateLetterIndices(publicBooksList); PluginManager pluginManager = this.getServer().getPluginManager(); diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java index 9e8bd07..c4598c8 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java @@ -10,11 +10,15 @@ import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.hover.content.Text; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; @@ -119,11 +123,21 @@ public final class BookFileHelper { */ public static void printBooks(@NotNull CommandSender sender, boolean listPublic, @NotNull String command, int page) { List availableBooks = BooksWithoutBorders.getAvailableBooks(sender, listPublic); + + Map firstInstances; + if (listPublic) { + firstInstances = BooksWithoutBorders.getLetterIndex(null); + } else if (sender instanceof Player player) { + firstInstances = BooksWithoutBorders.getLetterIndex(player.getUniqueId()); + } else { + firstInstances = new HashMap<>(); + } + int totalPages = (int) Math.ceil((double) availableBooks.size() / booksPerPage); if (page > totalPages) { sender.sendMessage(ChatColor.GRAY + "No such page"); } else { - showBookMenu(sender, command, page, totalPages, availableBooks); + showBookMenu(sender, command, page, totalPages, availableBooks, firstInstances); } } @@ -135,11 +149,28 @@ public final class BookFileHelper { * @param page

The currently selected page

* @param totalPages

The total amount of pages

* @param availableBooks

All books available to the sender

+ * @param firstInstances

The map between a character, and the index of the first instance of that character in the book list

*/ private static void showBookMenu(@NotNull CommandSender sender, @NotNull String command, int page, - int totalPages, @NotNull List availableBooks) { + int totalPages, @NotNull List availableBooks, + @NotNull Map firstInstances) { ComponentBuilder headerComponent = new ComponentBuilder(); headerComponent.append("Books page " + page + " of " + totalPages).color(ChatColor.GREEN); + + // Display links to first page where a letter can be found + for (int characterIndex = 0; characterIndex <= 25; characterIndex++) { + char character = (char) ('a' + characterIndex); + if (firstInstances.containsKey(character)) { + int pageIndex = (firstInstances.get(character) / booksPerPage) + 1; + headerComponent.append(" ", ComponentBuilder.FormatRetention.NONE).append(character + "").color( + ChatColor.AQUA).event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, + "/" + command + " page" + pageIndex)).event(new HoverEvent( + HoverEvent.Action.SHOW_TEXT, new Text("To page " + pageIndex))); + } else { + headerComponent.append(" " + character, ComponentBuilder.FormatRetention.NONE).color(ChatColor.GRAY); + } + } + sender.spigot().sendMessage(headerComponent.create()); // Print the previous page button @@ -148,26 +179,30 @@ public final class BookFileHelper { String fullCommand = "/" + command + " page" + (page - 1); HoverEvent prevPagePreview = new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("To page " + (page - 1))); ClickEvent prevPageClick = new ClickEvent(ClickEvent.Action.RUN_COMMAND, fullCommand); - previousComponent.append("Previous [<] " + ChatColor.DARK_GRAY + fullCommand).event(prevPagePreview).event(prevPageClick); + previousComponent.append("Previous [<]").event(prevPagePreview).event(prevPageClick).append(" " + + ChatColor.DARK_GRAY + fullCommand, ComponentBuilder.FormatRetention.NONE); } else { previousComponent.append("Previous [<]").color(ChatColor.GRAY); } sender.spigot().sendMessage(previousComponent.create()); // Print the main list of all book indexes and titles - HoverEvent interactSuggestion = new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click me")); int startIndex = (page - 1) * booksPerPage; for (int bookIndex = startIndex; bookIndex < Math.min(startIndex + booksPerPage, availableBooks.size()); bookIndex++) { ComponentBuilder bookComponent = new ComponentBuilder(); - TextComponent indexComponent = new TextComponent("[" + (bookIndex + 1) + "] "); + TextComponent indexComponent = new TextComponent("[" + (bookIndex + 1) + "]"); indexComponent.setColor(ChatColor.GOLD); bookComponent.append(indexComponent).event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, - "/" + command + " " + (bookIndex + 1))).event(interactSuggestion); + "/" + command + " " + (bookIndex + 1))).event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new Text("Select book by index"))); - TextComponent bookNameComponent = new TextComponent(availableBooks.get(bookIndex)); + bookComponent.append(" ", ComponentBuilder.FormatRetention.NONE); + + TextComponent bookNameComponent = new TextComponent(getNiceName(availableBooks.get(bookIndex))); bookNameComponent.setColor(ChatColor.WHITE); bookComponent.append(bookNameComponent).event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, - "/" + command + " " + availableBooks.get(bookIndex))).event(interactSuggestion); + "/" + command + " " + availableBooks.get(bookIndex))).event( + new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Select book by path"))); sender.spigot().sendMessage(bookComponent.create()); } @@ -177,13 +212,49 @@ public final class BookFileHelper { String fullCommand = "/" + command + " page" + (page + 1); HoverEvent nextPagePreview = new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("To page " + (page + 1))); ClickEvent nextPageClick = new ClickEvent(ClickEvent.Action.RUN_COMMAND, fullCommand); - nextComponent.append("Next [>] " + ChatColor.DARK_GRAY + fullCommand).event(nextPagePreview).event(nextPageClick); + nextComponent.append("Next [>]").event(nextPagePreview).event(nextPageClick).append(" " + + ChatColor.DARK_GRAY + fullCommand, ComponentBuilder.FormatRetention.NONE); } else { nextComponent.append("Next [>]").color(ChatColor.GRAY); } sender.spigot().sendMessage(nextComponent.create()); } + /** + * Gets a nice name from a book's path + * + * @param bookPath

The path of a book

+ * @return

The prettified book name

+ */ + @NotNull + private static String getNiceName(@NotNull String bookPath) { + return bookPath.substring(0, bookPath.length() - 4).replace(",", " by ").replace("_", " "); + } + + /** + * Gets a map between characters, and the first instance of a book's title starting with that character + * + * @param books

The books to look through

+ * @return

The map of the first index containing each character

+ */ + @NotNull + public static Map populateLetterIndices(@NotNull List books) { + List firstCharacter = new ArrayList<>(books.size()); + for (String bookIdentifier : books) { + firstCharacter.add(bookIdentifier.toLowerCase().charAt(0)); + } + + Map firstEncounter = new HashMap<>(); + for (int characterIndex = 0; characterIndex <= 25; characterIndex++) { + char character = (char) ('a' + characterIndex); + int index = Collections.binarySearch(firstCharacter, character); + if (index >= 0) { + firstEncounter.put(character, index); + } + } + return firstEncounter; + } + /** * Lists available files *