Complete tasks 7-9: GUI, utilities, integration

This commit is contained in:
Loki 2025-12-19 12:51:48 +01:00
parent 421ae1b4f3
commit 994fb6a109
9 changed files with 424 additions and 5 deletions

View file

@ -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. Утилиты и сообщения

View file

@ -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")
}

View file

@ -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) {

View file

@ -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;
}

View 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);
}
}
}

View 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;
}
}

View 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);
}
}

View 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));
}
}

View 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));
}
}