UIPosts

UIPosts.java v1.0
支持PlaceholderAPI的UI多端自定义界面及丰富帖子系统,数据驱动、多接口,集成网络时间,数据库保存帖子留言,UI种类支持Chest与书本,界面可互相跳转
作者: ScriptIrc Engine

命令列表

  • uiposts主操作命令,/uiposts chest 或 /uiposts book 可打开不同界面

权限列表

  • uiposts.adminUIPosts管理权限
package com.scriptaic.ui_posts;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
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.player.PlayerInteractEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.InputStreamReader;
import java.io.BufferedReader;

/**
 * @pluginName UIPosts
 * @author ScriptIrc Engine
 * @version 1.0
 * @description 支持PlaceholderAPI的UI多端自定义界面及丰富帖子系统,数据驱动、多接口,集成网络时间,数据库保存帖子留言,UI种类支持Chest与书本,界面可互相跳转
 * [command]uiposts|主操作命令,/uiposts chest 或 /uiposts book 可打开不同界面[/command]
 * [Permission]uiposts.admin|UIPosts管理权限[/Permission]
 */
public class UIPosts extends JavaPlugin implements Listener, TabCompleter {

    // SQLite数据库文件名
    private static final String DB_FILE = "uiposts.sqlite";
    private static final String TIME_API = "http://worldtimeapi.org/api/timezone/Asia/Shanghai";
    private Connection connection;
    private Map<String, Object> guiConfig; // 界面配置缓存

    @Override
    public void onEnable() {
        Bukkit.getPluginManager().registerEvents(this, this);
        getCommand("uiposts").setTabCompleter(this);
        initDatabase();
        loadConfig();
        getLogger().info("UIPosts 启动,已注册事件监听与命令.");
    }

    @Override
    public void onDisable() {
        try { if (connection != null && !connection.isClosed()) connection.close(); } catch (Exception ignored) {}
        getLogger().info("UIPosts 插件关闭.");
    }

    /* -------------------- 命令入口 ---------------------- */
    @Override
    public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        if (!(sender instanceof Player)) {
            sender.sendMessage("§c仅玩家可使用此命令.");
            return true;
        }
        Player p = (Player) sender;
        String type = (args.length==0) ? "chest" : args[0].toLowerCase();
        if (type.equals("chest")) {
            openChestGUI(p, 1);
        } else if (type.equals("book")) {
            openBookGUI(p, 1);
        } else {
            p.sendMessage(ChatColor.YELLOW + "/uiposts chest 或 /uiposts book 打开不同界面");
        }
        return true;
    }

    /* --------- Tab补全: chest/book ----------- */
    @Override
    public List<String> onTabComplete(CommandSender s, Command c, String l, String[] a) {
        if (a.length == 1) return Arrays.asList("chest", "book");
        return null;
    }

    /* ---------- 配置加载: 直接用嵌入结构 ------- */
    private void loadConfig() {
        guiConfig = new HashMap<>();
        // chestConfig
        Map<String, Object> chestGUI = new HashMap<>();
        chestGUI.put("title", "§b§l帖子广场 [Chest]页{page}/{pages}");
        chestGUI.put("size", 27);
        chestGUI.put("post_slot", Arrays.asList(10, 11, 12, 13, 14));
        chestGUI.put("next_slot", 25);
        chestGUI.put("prev_slot", 19);
        chestGUI.put("post_button", "§a§l我要发帖");
        chestGUI.put("jump_book", "§6查看书本模式");
        guiConfig.put("chest", chestGUI);
        // bookConfig
        Map<String, Object> bookGUI = new HashMap<>();
        bookGUI.put("title", "§b§l帖子广场 [书本]第{page}页");
        bookGUI.put("lines_per_page", 7);
        guiConfig.put("book", bookGUI);
    }

    /* ---------- 数据库 ----------- */
    private void initDatabase() {
        try {
            File dbFile = new File(getDataFolder(), DB_FILE);
            if (!getDataFolder().exists()) getDataFolder().mkdirs();
            boolean init = false;
            if (!dbFile.exists()) { dbFile.createNewFile(); init=true; }
            connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getPath());
            if (init) {
                Statement st = connection.createStatement();
                // 帖子表
                st.executeUpdate("CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, player VARCHAR, content TEXT, time VARCHAR)");
                // 留言表
                st.executeUpdate("CREATE TABLE IF NOT EXISTS replies (id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER, player VARCHAR, content TEXT, time VARCHAR)");
                st.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /* =========== UI核心实现 =========== */
    // 打开Chest GUI, page从1开始
    private void openChestGUI(Player p, int page) {
        Map<String, Object> cfg = (Map<String, Object>) guiConfig.get("chest");
        int size = (int) cfg.get("size");
        String title = (String) cfg.get("title");
        List<Integer> postSlots = (List<Integer>) cfg.get("post_slot");
        Inventory inv = Bukkit.createInventory(null, size, parse(title, p, page, getTotalPages(postSlots.size())));
        List<Post> posts = getPostsByPage(page, postSlots.size());
        // 填充帖子
        int i = 0;
        for (; i < posts.size() && i < postSlots.size(); i++) {
            Post post = posts.get(i);
            ItemStack item = new ItemStack(Material.WRITABLE_BOOK);
            ItemMeta im = item.getItemMeta();
            im.setDisplayName(ChatColor.GREEN + "【" + post.player + "】 - " + post.time);
            List<String> lore = Arrays.asList(ChatColor.WHITE + post.content, ChatColor.GRAY + "点击查看/留言");
            im.setLore(lore);
            item.setItemMeta(im);
            inv.setItem(postSlots.get(i), item);
        }
        // 发帖按钮
        ItemStack postBtn = new ItemStack(Material.ANVIL);
        ItemMeta pm = postBtn.getItemMeta();
        pm.setDisplayName((String) cfg.get("post_button"));
        postBtn.setItemMeta(pm);
        inv.setItem(4, postBtn);
        // 跳转到书按钮
        ItemStack jumpBtn = new ItemStack(Material.WRITTEN_BOOK);
        ItemMeta jm = jumpBtn.getItemMeta();
        jm.setDisplayName((String) cfg.get("jump_book"));
        jumpBtn.setItemMeta(jm);
        inv.setItem(22, jumpBtn);
        // 分页按钮
        ItemStack nextBtn = new ItemStack(Material.ARROW);
        ItemMeta nm = nextBtn.getItemMeta();
        nm.setDisplayName("§e下一页");
        nextBtn.setItemMeta(nm);
        inv.setItem((int) cfg.get("next_slot"), nextBtn);
        ItemStack prevBtn = new ItemStack(Material.ARROW);
        ItemMeta pmv = prevBtn.getItemMeta();
        pmv.setDisplayName("§e上一页");
        prevBtn.setItemMeta(pmv);
        inv.setItem((int) cfg.get("prev_slot"), prevBtn);

        p.openInventory(inv);
        // 标记打开的page
        p.setMetadata("uiposts_gui_page", new org.bukkit.metadata.FixedMetadataValue(this, page));
        p.setMetadata("uiposts_gui_type", new org.bukkit.metadata.FixedMetadataValue(this, "chest"));
    }

    // 打开 Book GUI, page从1开始
    private void openBookGUI(Player p, int page) {
        Map<String, Object> cfg = (Map<String, Object>) guiConfig.get("book");
        int lines = (int) cfg.get("lines_per_page");
        String title = (String) cfg.get("title");
        List<Post> posts = getPostsByPage(page, lines);
        ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
        BookMeta meta = (BookMeta) book.getItemMeta();
        List<String> pages = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < posts.size(); i++) {
            Post post = posts.get(i);
            sb.append(ChatColor.BLUE+"【"+post.player+ "】 ").append(post.time).append("\n");
            sb.append(ChatColor.RESET).append(post.content).append("\n");
            // 获取留言
            List<Reply> replies = getReplies(post.id);
            for (Reply r : replies) {
                sb.append(ChatColor.GRAY+"  - "+r.player+":"+r.content+ " ").append("\n");
            }
            sb.append("\n");
            if ((i+1)%lines == 0 || i==posts.size()-1) {
                sb.append("§0§n翻页/跳Chest输入 /uiposts chest");
                pages.add(sb.toString());
                sb.setLength(0);
            }
        }
        if (pages.isEmpty()) pages.add("§7暂无帖子, §a发帖见Chest界面!");
        meta.setAuthor("UIPosts");
        meta.setTitle(parse(title,p,page,getTotalPages(lines)));
        meta.setPages(pages);
        book.setItemMeta(meta);
        // Give book&open
        int old = p.getInventory().getHeldItemSlot();
        ItemStack oldItem = p.getInventory().getItem(old);
        p.getInventory().setItem(old, book);
        (new BukkitRunnable(){
            public void run(){ p.openBook(book);}
        }).runTaskLater(this, 2L);
        // 标记打开的page
        p.setMetadata("uiposts_gui_page", new org.bukkit.metadata.FixedMetadataValue(this, page));
        p.setMetadata("uiposts_gui_type", new org.bukkit.metadata.FixedMetadataValue(this, "book"));
    }

    /* ---------- 事件监听核心 ----------- */
    @EventHandler
    public void onInvClick(InventoryClickEvent e) {
        Player p = (Player) e.getWhoClicked();
        if (!p.hasMetadata("uiposts_gui_type") || !p.hasMetadata("uiposts_gui_page")) return;
        if (!(e.getView().getTitle().contains("帖子广场"))) return;
        e.setCancelled(true);
        int page = p.getMetadata("uiposts_gui_page").get(0).asInt();
        String type = p.getMetadata("uiposts_gui_type").get(0).asString();
        ItemStack current = e.getCurrentItem();
        if (current==null || !current.hasItemMeta()) return;
        String name = ChatColor.stripColor(current.getItemMeta().getDisplayName());
        // 发帖按钮
        if (name.contains("我要发帖")) {
            p.closeInventory();
            askForPostInput(p);
            return;
        }
        // 跳转到书本
        if (name.contains("查看书本模式")) {
            p.closeInventory();
            openBookGUI(p, 1);
            return;
        }
        // 分页
        if (name.contains("下一页")) {
            openChestGUI(p, page+1);
            return;
        }
        if (name.contains("上一页")) {
            openChestGUI(p, Math.max(1, page-1));
            return;
        }
        // 查看帖子(用book显示并留言)
        if (current.getType()==Material.WRITABLE_BOOK) {
            int idx = ((List<Integer>)((Map<String,Object>)guiConfig.get("chest")).get("post_slot")).indexOf(e.getSlot());
            List<Post> posts = getPostsByPage(page, ((List<Integer>)((Map<String,Object>)guiConfig.get("chest")).get("post_slot")).size());
            if (idx<0 || idx>=posts.size()) return;
            showBookForPost(p, posts.get(idx));
        }
    }
    // 简单演示: 书本一点击即跳Chest, 你可以拓展翻页
    @EventHandler
    public void onPlayerInteract(PlayerInteractEvent e) {
        Player p = e.getPlayer();
        if (p.hasMetadata("uiposts_gui_type") && "book".equals(p.getMetadata("uiposts_gui_type").get(0).asString())) {
            openChestGUI(p, 1);
        }
    }

    /* ----------- 发帖/留言逻辑模拟 ------------ */
    // 发帖输入简单采用聊天监听(可拓展对话库)
    private Map<UUID, Boolean> awaitingPostInput = new HashMap<>();
    private Map<UUID, Integer> awaitingReply = new HashMap<>();
    private void askForPostInput(Player p) {
        p.sendMessage("§e请输入您的帖子内容,限100字内(输入 \u53d6\u6d88 取消):");
        awaitingPostInput.put(p.getUniqueId(), true);
    }
    private void askForReplyInput(Player p, int postId) {
        p.sendMessage("§e请输入您的留言内容,限50字内(输入 \u53d6\u6d88 取消):");
        awaitingReply.put(p.getUniqueId(), postId);
    }

    // 聊天事件用于发帖/留言
    @EventHandler
    public void onPlayerChat(org.bukkit.event.player.AsyncPlayerChatEvent e) {
        UUID uid = e.getPlayer().getUniqueId();
        if (awaitingPostInput.containsKey(uid)) {
            e.setCancelled(true);
            String msg = e.getMessage();
            if ("取消".equals(msg)) {
                awaitingPostInput.remove(uid);
                e.getPlayer().sendMessage("§c已取消发帖");
                return;
            }
            savePost(e.getPlayer(), msg.length()>100 ? msg.substring(0,100) : msg);
            awaitingPostInput.remove(uid);
            openChestGUI(e.getPlayer(), 1);
            return;
        }
        if (awaitingReply.containsKey(uid)) {
            e.setCancelled(true);
            String msg = e.getMessage();
            if ("取消".equals(msg)) {
                awaitingReply.remove(uid);
                e.getPlayer().sendMessage("§c已取消留言");
                return;
            }
            saveReply(e.getPlayer(), awaitingReply.get(uid), msg.length()>50 ? msg.substring(0,50) : msg);
            awaitingReply.remove(uid);
            openBookGUI(e.getPlayer(), 1);
            return;
        }
    }

    // 保存帖子、留言
    private void savePost(Player p, String content) {
        String time = getInternetTime();
        try {
            PreparedStatement ps = connection.prepareStatement("INSERT INTO posts(player,content,time) VALUES(?, ?, ?)");
            ps.setString(1, p.getName());
            ps.setString(2, content);
            ps.setString(3, time);
            ps.executeUpdate();
            ps.close();
        } catch (SQLException e) { e.printStackTrace(); }
    }
    private void saveReply(Player p, int postId, String content) {
        String time = getInternetTime();
        try {
            PreparedStatement ps = connection.prepareStatement("INSERT INTO replies(post_id,player,content,time) VALUES(?, ?, ?, ?)");
            ps.setInt(1, postId);
            ps.setString(2, p.getName());
            ps.setString(3, content);
            ps.setString(4, time);
            ps.executeUpdate();
            ps.close();
        } catch (SQLException e) { e.printStackTrace(); }
    }

    // 获取分页帖子最新
    private List<Post> getPostsByPage(int page, int size) {
        List<Post> res = new ArrayList<>();
        try {
            PreparedStatement ps = connection.prepareStatement("SELECT id,player,content,time FROM posts ORDER BY id DESC LIMIT ? OFFSET ?");
            ps.setInt(1, size);
            ps.setInt(2, (page-1)*size);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) res.add(new Post(rs.getInt("id"), rs.getString("player"), rs.getString("content"), rs.getString("time")));
            rs.close(); ps.close();
        } catch (SQLException e) {}
        return res;
    }
    // 总页数
    private int getTotalPages(int pageSize) {
        try {
            PreparedStatement ps = connection.prepareStatement("SELECT COUNT(*) FROM posts");
            ResultSet rs = ps.executeQuery();
            int count = rs.next() ? rs.getInt(1) : 0;
            rs.close(); ps.close();
            return Math.max(1, (count+pageSize-1)/pageSize);
        } catch (SQLException e) {}
        return 1;
    }

    // 获取留言
    private List<Reply> getReplies(int postId) {
        List<Reply> res = new ArrayList<>();
        try {
            PreparedStatement ps = connection.prepareStatement("SELECT player,content,time FROM replies WHERE post_id=? ORDER BY id ASC");
            ps.setInt(1, postId);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) res.add(new Reply(rs.getString("player"), rs.getString("content"), rs.getString("time")));
            rs.close(); ps.close();
        } catch (SQLException e) {}
        return res;
    }
    // 帖子详情展示&留言入口 (书本)
    private void showBookForPost(Player p, Post post) {
        ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
        BookMeta meta = (BookMeta) book.getItemMeta();
        StringBuilder sb = new StringBuilder();
        sb.append(ChatColor.BLUE+"【"+post.player+ "】 ").append(post.time).append("\n");
        sb.append(ChatColor.RESET).append(post.content).append("\n");
        List<Reply> replies = getReplies(post.id);
        for (Reply r : replies) {
            sb.append(ChatColor.GRAY+"  - "+r.player+":"+r.content+ " ").append("\n");
        }
        sb.append("\n§b留言请在聊天输入/‘取消’退出");
        meta.setAuthor("UIPosts");
        meta.setTitle("回复帖子");
        meta.setPages(Collections.singletonList(sb.toString()));
        book.setItemMeta(meta);
        p.getInventory().setItem(p.getInventory().getHeldItemSlot(), book);
        (new BukkitRunnable(){ public void run(){ p.openBook(book);}}).runTaskLater(this, 2L);
        // 标记等待留言
        askForReplyInput(p, post.id);
    }

    /* ---------- 占位符支持演示(需要 PlaceholderAPI 已加载) ---------- */
    // 演示: {player} {page} {pages} 可用, 并可插入 PlaceholderAPI 所有占位符
    private String parse(String raw, Player p, int page, int pages) {
        String t = raw.replace("{player}", p.getName()).replace("{page}", String.valueOf(page)).replace("{pages}", String.valueOf(pages));
        if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
            try { // PlaceholderAPI hook
                return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(p, t);
            } catch (Throwable ignore) {}
        }
        return t;
    }

    /* ---------- 获取网络当前时间 ---------- */
    private String getInternetTime() {
        try {
            URL url = new URL(TIME_API);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(1500); conn.setReadTimeout(2000);
            conn.setRequestMethod("GET");
            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line, json = "";
            while ((line = in.readLine()) != null) json += line;
            in.close();
            int idx = json.indexOf("datetime");
            if (idx!=-1) {
                String val = json.substring(idx+11, idx+30);
                return val.replace("T", " ").replace(",\"day_of_week", "").split("[+Z]")[0];
            }
        } catch (Exception e) {}
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    }

    /* =========== 数据结构 =========== */
    private static class Post {
        int id; String player, content, time;
        Post(int id, String player, String content, String time) {this.id=id;this.player=player;this.content=content;this.time=time;}
    }
    private static class Reply {
        String player, content, time;
        Reply(String pl, String c, String t) {player=pl;content=c;time=t;}
    }
}

上一篇: RewardKill下一篇: UIPosts

举报内容

意见反馈