Complete tasks 7-9: GUI, utilities, integration
This commit is contained in:
parent
421ae1b4f3
commit
994fb6a109
9 changed files with 424 additions and 5 deletions
|
|
@ -116,22 +116,41 @@
|
|||
- **Property 8: Permission Enforcement**
|
||||
- **Validates: Requirements 6.1, 6.2, 6.3**
|
||||
|
||||
- [ ] 7. Реализация GUI
|
||||
- [x] 7. Реализация GUI
|
||||
|
||||
|
||||
|
||||
|
||||
- [ ] 7.1 Создать ReportGui с пагинацией
|
||||
- Создать инвентарь 54 слота
|
||||
- Отображение голов игроков с их скинами
|
||||
- Lore с информацией о репорте
|
||||
|
||||
|
||||
- Кнопки навигации по страницам
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||
- [ ] 7.2 Создать GuiClickListener
|
||||
|
||||
- [-] 7.2 Создать GuiClickListener
|
||||
|
||||
|
||||
|
||||
- Обработка кликов по страницам
|
||||
|
||||
|
||||
- Отмена выноса предметов
|
||||
|
||||
- _Requirements: 4.4_
|
||||
|
||||
|
||||
- [ ] 7.3 Создать ReportGuiCommand (/report gui)
|
||||
- Проверка прав и открытие GUI
|
||||
|
||||
|
||||
- _Requirements: 4.1, 6.2_
|
||||
|
||||
- [ ] 7.4 Написать property test для пагинации
|
||||
- **Property 6: Pagination Correctness**
|
||||
|
||||
- **Validates: Requirements 4.4**
|
||||
|
||||
- [ ] 8. Утилиты и сообщения
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ dependencies {
|
|||
testImplementation("net.jqwik:jqwik:1.8.4")
|
||||
testImplementation("org.yaml:snakeyaml:2.2")
|
||||
testImplementation("org.xerial:sqlite-jdbc:3.45.1.0")
|
||||
testCompileOnly("io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package dev.loki.lorep;
|
||||
|
||||
import dev.loki.lorep.command.ReportCommand;
|
||||
import dev.loki.lorep.config.ConfigManager;
|
||||
import dev.loki.lorep.database.DatabaseManager;
|
||||
import dev.loki.lorep.database.PostgreSQLDatabaseManager;
|
||||
import dev.loki.lorep.database.SQLiteDatabaseManager;
|
||||
import dev.loki.lorep.gui.GuiClickListener;
|
||||
import dev.loki.lorep.webhook.DiscordWebhook;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
|
|
@ -17,11 +21,58 @@ public final class LorepPlugin extends JavaPlugin {
|
|||
instance = this;
|
||||
getLogger().info("lorep plugin enabling...");
|
||||
|
||||
// Initialize managers (will be implemented later)
|
||||
// Initialize config
|
||||
configManager = new ConfigManager(this);
|
||||
|
||||
// Initialize database
|
||||
initializeDatabase();
|
||||
|
||||
// Initialize webhook
|
||||
String webhookUrl = configManager.getWebhookUrl();
|
||||
if (webhookUrl != null && !webhookUrl.isEmpty()) {
|
||||
discordWebhook = new DiscordWebhook(webhookUrl, getLogger());
|
||||
getLogger().info("Discord webhook configured");
|
||||
} else {
|
||||
getLogger().warning("Discord webhook URL not configured");
|
||||
}
|
||||
|
||||
// Register commands
|
||||
ReportCommand reportCommand = new ReportCommand(this);
|
||||
getCommand("report").setExecutor(reportCommand);
|
||||
getCommand("report").setTabCompleter(reportCommand);
|
||||
|
||||
// Register listeners
|
||||
getServer().getPluginManager().registerEvents(new GuiClickListener(this), this);
|
||||
|
||||
getLogger().info("lorep plugin enabled!");
|
||||
}
|
||||
|
||||
private void initializeDatabase() {
|
||||
String dbType = configManager.getDatabaseType();
|
||||
|
||||
if ("postgresql".equalsIgnoreCase(dbType)) {
|
||||
databaseManager = new PostgreSQLDatabaseManager(
|
||||
configManager.getPostgresHost(),
|
||||
configManager.getPostgresPort(),
|
||||
configManager.getPostgresDatabase(),
|
||||
configManager.getPostgresUsername(),
|
||||
configManager.getPostgresPassword(),
|
||||
configManager.getPostgresPoolSize(),
|
||||
getLogger()
|
||||
);
|
||||
getLogger().info("Using PostgreSQL database");
|
||||
} else {
|
||||
databaseManager = new SQLiteDatabaseManager(
|
||||
getDataFolder(),
|
||||
configManager.getSqliteFile(),
|
||||
getLogger()
|
||||
);
|
||||
getLogger().info("Using SQLite database");
|
||||
}
|
||||
|
||||
databaseManager.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (databaseManager != null) {
|
||||
|
|
|
|||
|
|
@ -109,8 +109,7 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
|
|||
return true;
|
||||
}
|
||||
|
||||
// GUI will be implemented in task 7
|
||||
player.sendMessage("§eGUI будет реализован позже");
|
||||
new dev.loki.lorep.gui.ReportGui(plugin, 0).open(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
59
src/main/java/dev/loki/lorep/gui/GuiClickListener.java
Normal file
59
src/main/java/dev/loki/lorep/gui/GuiClickListener.java
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package dev.loki.lorep.gui;
|
||||
|
||||
import dev.loki.lorep.LorepPlugin;
|
||||
import dev.loki.lorep.util.PaginationUtil;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
|
||||
public class GuiClickListener implements Listener {
|
||||
|
||||
private final LorepPlugin plugin;
|
||||
|
||||
public GuiClickListener(LorepPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (!(event.getInventory().getHolder() instanceof ReportGui gui)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
if (!(event.getWhoClicked() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
// Navigation buttons are in bottom row (slots 45-53)
|
||||
switch (slot) {
|
||||
case 45 -> { // Previous page
|
||||
if (gui.getPage() > 0) {
|
||||
new ReportGui(plugin, gui.getPage() - 1).open(player);
|
||||
}
|
||||
}
|
||||
case 47 -> { // Close
|
||||
player.closeInventory();
|
||||
}
|
||||
case 53 -> { // Next page
|
||||
int totalReports = plugin.getDatabaseManager().getTotalReportCount();
|
||||
int totalPages = PaginationUtil.calculateTotalPages(totalReports, ReportGui.PAGE_SIZE);
|
||||
if (gui.getPage() < totalPages - 1) {
|
||||
new ReportGui(plugin, gui.getPage() + 1).open(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (event.getInventory().getHolder() instanceof ReportGui) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/main/java/dev/loki/lorep/gui/ReportGui.java
Normal file
135
src/main/java/dev/loki/lorep/gui/ReportGui.java
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package dev.loki.lorep.gui;
|
||||
|
||||
import dev.loki.lorep.LorepPlugin;
|
||||
import dev.loki.lorep.database.Report;
|
||||
import dev.loki.lorep.util.TimeUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ReportGui implements InventoryHolder {
|
||||
|
||||
public static final int PAGE_SIZE = 45; // 5 rows for reports
|
||||
public static final int INVENTORY_SIZE = 54; // 6 rows total
|
||||
|
||||
private final LorepPlugin plugin;
|
||||
private final int page;
|
||||
private Inventory inventory;
|
||||
|
||||
public ReportGui(LorepPlugin plugin, int page) {
|
||||
this.plugin = plugin;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
String title = plugin.getConfigManager().getMessage("gui-title", "%page%", String.valueOf(page + 1));
|
||||
this.inventory = Bukkit.createInventory(this, INVENTORY_SIZE, title);
|
||||
|
||||
List<Report> reports = plugin.getDatabaseManager().getReportsPaginated(page, PAGE_SIZE);
|
||||
int totalReports = plugin.getDatabaseManager().getTotalReportCount();
|
||||
|
||||
// Add report items
|
||||
for (int i = 0; i < reports.size(); i++) {
|
||||
Report report = reports.get(i);
|
||||
int targetReportCount = plugin.getDatabaseManager().getReportCount(report.targetUuid());
|
||||
inventory.setItem(i, createReportItem(report, targetReportCount));
|
||||
}
|
||||
|
||||
// Add navigation buttons
|
||||
addNavigationButtons(totalReports);
|
||||
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private ItemStack createReportItem(Report report, int totalReports) {
|
||||
ItemStack head = new ItemStack(Material.PLAYER_HEAD);
|
||||
SkullMeta meta = (SkullMeta) head.getItemMeta();
|
||||
|
||||
if (meta != null) {
|
||||
// Set skull owner for skin
|
||||
meta.setOwningPlayer(Bukkit.getOfflinePlayer(report.targetUuid()));
|
||||
|
||||
// Display name
|
||||
meta.setDisplayName("§c" + report.targetName());
|
||||
|
||||
// Lore with report info
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7");
|
||||
lore.add("§eОтправитель: §f" + report.reporterName());
|
||||
lore.add("§ePричина: §f" + report.reason());
|
||||
lore.add("§eВремя: §f" + TimeUtil.formatTimeAgo(report.createdAt().toEpochMilli()));
|
||||
lore.add("§7");
|
||||
lore.add("§eВсего репортов: §c" + totalReports);
|
||||
|
||||
meta.setLore(lore);
|
||||
head.setItemMeta(meta);
|
||||
}
|
||||
|
||||
return head;
|
||||
}
|
||||
|
||||
private void addNavigationButtons(int totalReports) {
|
||||
int totalPages = (int) Math.ceil((double) totalReports / PAGE_SIZE);
|
||||
|
||||
// Previous page button (slot 45)
|
||||
if (page > 0) {
|
||||
ItemStack prevButton = new ItemStack(Material.ARROW);
|
||||
ItemMeta prevMeta = prevButton.getItemMeta();
|
||||
if (prevMeta != null) {
|
||||
prevMeta.setDisplayName("§a← Предыдущая страница");
|
||||
prevButton.setItemMeta(prevMeta);
|
||||
}
|
||||
inventory.setItem(45, prevButton);
|
||||
}
|
||||
|
||||
// Page info (slot 49)
|
||||
ItemStack pageInfo = new ItemStack(Material.PAPER);
|
||||
ItemMeta pageMeta = pageInfo.getItemMeta();
|
||||
if (pageMeta != null) {
|
||||
pageMeta.setDisplayName("§eСтраница " + (page + 1) + " из " + Math.max(1, totalPages));
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7Всего репортов: " + totalReports);
|
||||
pageMeta.setLore(lore);
|
||||
pageInfo.setItemMeta(pageMeta);
|
||||
}
|
||||
inventory.setItem(49, pageInfo);
|
||||
|
||||
// Next page button (slot 53)
|
||||
if (page < totalPages - 1) {
|
||||
ItemStack nextButton = new ItemStack(Material.ARROW);
|
||||
ItemMeta nextMeta = nextButton.getItemMeta();
|
||||
if (nextMeta != null) {
|
||||
nextMeta.setDisplayName("§aСледующая страница →");
|
||||
nextButton.setItemMeta(nextMeta);
|
||||
}
|
||||
inventory.setItem(53, nextButton);
|
||||
}
|
||||
|
||||
// Close button (slot 47)
|
||||
ItemStack closeButton = new ItemStack(Material.BARRIER);
|
||||
ItemMeta closeMeta = closeButton.getItemMeta();
|
||||
if (closeMeta != null) {
|
||||
closeMeta.setDisplayName("§cЗакрыть");
|
||||
closeButton.setItemMeta(closeMeta);
|
||||
}
|
||||
inventory.setItem(47, closeButton);
|
||||
}
|
||||
|
||||
public int getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/java/dev/loki/lorep/util/MessageUtil.java
Normal file
34
src/main/java/dev/loki/lorep/util/MessageUtil.java
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package dev.loki.lorep.util;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class MessageUtil {
|
||||
|
||||
private static final String PREFIX = "§8[§clorep§8] §r";
|
||||
|
||||
public static String colorize(String message) {
|
||||
return ChatColor.translateAlternateColorCodes('&', message);
|
||||
}
|
||||
|
||||
public static void send(CommandSender sender, String message) {
|
||||
sender.sendMessage(PREFIX + colorize(message));
|
||||
}
|
||||
|
||||
public static void sendRaw(CommandSender sender, String message) {
|
||||
sender.sendMessage(colorize(message));
|
||||
}
|
||||
|
||||
public static void sendNoPrefix(Player player, String message) {
|
||||
player.sendMessage(colorize(message));
|
||||
}
|
||||
|
||||
public static String format(String message, String... replacements) {
|
||||
String result = message;
|
||||
for (int i = 0; i < replacements.length - 1; i += 2) {
|
||||
result = result.replace(replacements[i], replacements[i + 1]);
|
||||
}
|
||||
return colorize(result);
|
||||
}
|
||||
}
|
||||
19
src/main/java/dev/loki/lorep/util/PaginationUtil.java
Normal file
19
src/main/java/dev/loki/lorep/util/PaginationUtil.java
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package dev.loki.lorep.util;
|
||||
|
||||
public class PaginationUtil {
|
||||
|
||||
public static int calculateTotalPages(int totalItems, int pageSize) {
|
||||
if (totalItems <= 0) return 1;
|
||||
return (int) Math.ceil((double) totalItems / pageSize);
|
||||
}
|
||||
|
||||
public static int calculateStartIndex(int page, int pageSize) {
|
||||
return page * pageSize;
|
||||
}
|
||||
|
||||
public static int calculateItemsOnPage(int page, int pageSize, int totalItems) {
|
||||
int startIndex = calculateStartIndex(page, pageSize);
|
||||
int remaining = totalItems - startIndex;
|
||||
return Math.min(pageSize, Math.max(0, remaining));
|
||||
}
|
||||
}
|
||||
102
src/test/java/dev/loki/lorep/gui/PaginationTest.java
Normal file
102
src/test/java/dev/loki/lorep/gui/PaginationTest.java
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package dev.loki.lorep.gui;
|
||||
|
||||
import dev.loki.lorep.util.PaginationUtil;
|
||||
import net.jqwik.api.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* **Feature: lorep-report-plugin, Property 6: Pagination Correctness**
|
||||
* **Validates: Requirements 4.4**
|
||||
*/
|
||||
public class PaginationTest {
|
||||
|
||||
private static final int PAGE_SIZE = 45;
|
||||
|
||||
/**
|
||||
* **Property 6: Pagination Correctness**
|
||||
* For any list of N reports and page size P, requesting page K SHALL return
|
||||
* at most P reports starting from index K*P, and the total page count SHALL
|
||||
* equal ceil(N/P).
|
||||
* **Validates: Requirements 4.4**
|
||||
*/
|
||||
@Property(tries = 100)
|
||||
void paginationCorrectness(
|
||||
@ForAll("totalReports") int totalReports,
|
||||
@ForAll("pageNumbers") int page
|
||||
) {
|
||||
int totalPages = PaginationUtil.calculateTotalPages(totalReports, PAGE_SIZE);
|
||||
int startIndex = PaginationUtil.calculateStartIndex(page, PAGE_SIZE);
|
||||
int itemsOnPage = PaginationUtil.calculateItemsOnPage(page, PAGE_SIZE, totalReports);
|
||||
|
||||
// Total pages should equal ceil(N/P)
|
||||
int expectedTotalPages = totalReports <= 0 ? 1 : (int) Math.ceil((double) totalReports / PAGE_SIZE);
|
||||
assertEquals(expectedTotalPages, totalPages,
|
||||
"Total pages should equal ceil(totalReports/pageSize)");
|
||||
|
||||
// Start index should be page * pageSize
|
||||
assertEquals(page * PAGE_SIZE, startIndex,
|
||||
"Start index should be page * pageSize");
|
||||
|
||||
// Items on page should be at most PAGE_SIZE
|
||||
assertTrue(itemsOnPage <= PAGE_SIZE,
|
||||
"Items on page should not exceed page size");
|
||||
|
||||
// Items on page should be non-negative
|
||||
assertTrue(itemsOnPage >= 0,
|
||||
"Items on page should be non-negative");
|
||||
|
||||
// For valid pages, items should match expected
|
||||
if (page < totalPages && page >= 0) {
|
||||
int expectedItems = Math.min(PAGE_SIZE, totalReports - startIndex);
|
||||
assertEquals(Math.max(0, expectedItems), itemsOnPage,
|
||||
"Items on valid page should match expected count");
|
||||
}
|
||||
}
|
||||
|
||||
@Property(tries = 100)
|
||||
void allItemsCoveredByPages(@ForAll("totalReports") int totalReports) {
|
||||
Assume.that(totalReports > 0);
|
||||
|
||||
int totalPages = PaginationUtil.calculateTotalPages(totalReports, PAGE_SIZE);
|
||||
int totalItemsCovered = 0;
|
||||
|
||||
for (int page = 0; page < totalPages; page++) {
|
||||
totalItemsCovered += PaginationUtil.calculateItemsOnPage(page, PAGE_SIZE, totalReports);
|
||||
}
|
||||
|
||||
assertEquals(totalReports, totalItemsCovered,
|
||||
"Sum of items across all pages should equal total reports");
|
||||
}
|
||||
|
||||
@Provide
|
||||
Arbitrary<Integer> totalReports() {
|
||||
return Arbitraries.integers().between(0, 500);
|
||||
}
|
||||
|
||||
@Provide
|
||||
Arbitrary<Integer> pageNumbers() {
|
||||
return Arbitraries.integers().between(0, 20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyReportsHasOnePage() {
|
||||
assertEquals(1, PaginationUtil.calculateTotalPages(0, PAGE_SIZE));
|
||||
assertEquals(0, PaginationUtil.calculateItemsOnPage(0, PAGE_SIZE, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactlyOnePageOfReports() {
|
||||
assertEquals(1, PaginationUtil.calculateTotalPages(PAGE_SIZE, PAGE_SIZE));
|
||||
assertEquals(PAGE_SIZE, PaginationUtil.calculateItemsOnPage(0, PAGE_SIZE, PAGE_SIZE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lastPageHasRemainingItems() {
|
||||
int totalReports = PAGE_SIZE + 10;
|
||||
assertEquals(2, PaginationUtil.calculateTotalPages(totalReports, PAGE_SIZE));
|
||||
assertEquals(PAGE_SIZE, PaginationUtil.calculateItemsOnPage(0, PAGE_SIZE, totalReports));
|
||||
assertEquals(10, PaginationUtil.calculateItemsOnPage(1, PAGE_SIZE, totalReports));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue