janky ui + /sc <message> exists now
This commit is contained in:
@@ -139,7 +139,7 @@ public final class StaffPlugin extends JavaPlugin {
|
|||||||
getCommandRegistry().registerCommand(new PunishmentCommands.Warn(punishmentService, staffChat, resolver));
|
getCommandRegistry().registerCommand(new PunishmentCommands.Warn(punishmentService, staffChat, resolver));
|
||||||
getCommandRegistry().registerCommand(new PunishmentCommands.Unban(punishmentService, staffChat, resolver));
|
getCommandRegistry().registerCommand(new PunishmentCommands.Unban(punishmentService, staffChat, resolver));
|
||||||
getCommandRegistry().registerCommand(new PunishmentCommands.Unmute(punishmentService, staffChat, resolver));
|
getCommandRegistry().registerCommand(new PunishmentCommands.Unmute(punishmentService, staffChat, resolver));
|
||||||
getCommandRegistry().registerCommand(new StaffChatCommand(chatToggle));
|
getCommandRegistry().registerCommand(new StaffChatCommand(chatToggle, staffChat));
|
||||||
getCommandRegistry().registerCommand(new LookupCommand(resolver, seenRepo, punishmentService));
|
getCommandRegistry().registerCommand(new LookupCommand(resolver, seenRepo, punishmentService));
|
||||||
getCommandRegistry().registerCommand(new HistoryCommand(resolver, punishmentRepo));
|
getCommandRegistry().registerCommand(new HistoryCommand(resolver, punishmentRepo));
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,35 @@ import com.hypixel.hytale.component.Ref;
|
|||||||
import com.hypixel.hytale.component.Store;
|
import com.hypixel.hytale.component.Store;
|
||||||
import com.hypixel.hytale.server.core.Message;
|
import com.hypixel.hytale.server.core.Message;
|
||||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
import com.hypixel.hytale.server.core.universe.world.World;
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
import net.kewwbec.staff.StaffPermissionsNodes;
|
import net.kewwbec.staff.StaffPermissionsNodes;
|
||||||
|
import net.kewwbec.staff.service.StaffChatService;
|
||||||
import net.kewwbec.staff.service.StaffChatToggleService;
|
import net.kewwbec.staff.service.StaffChatToggleService;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /sc [message...]
|
||||||
|
*
|
||||||
|
* If a body is supplied, sends it once to staff chat without changing your chat mode.
|
||||||
|
* If no body, toggles staff chat mode - all subsequent chat goes to staff until /sc is run again.
|
||||||
|
*/
|
||||||
public final class StaffChatCommand extends AbstractPlayerCommand {
|
public final class StaffChatCommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
private final StaffChatToggleService toggle;
|
private final StaffChatToggleService toggle;
|
||||||
|
private final StaffChatService staffChat;
|
||||||
|
private final OptionalArg<String> bodyArg = withOptionalArg("message", "Message body (optional)", ArgTypes.GREEDY_STRING);
|
||||||
|
|
||||||
public StaffChatCommand(@Nonnull StaffChatToggleService toggle) {
|
public StaffChatCommand(@Nonnull StaffChatToggleService toggle, @Nonnull StaffChatService staffChat) {
|
||||||
super("sc", "Toggle staff chat mode (your chat goes to staff instead of world)");
|
super("sc", "Send a one-shot staff message, or toggle staff chat mode if no message given");
|
||||||
this.toggle = toggle;
|
this.toggle = toggle;
|
||||||
|
this.staffChat = staffChat;
|
||||||
requirePermission(StaffPermissionsNodes.CHAT);
|
requirePermission(StaffPermissionsNodes.CHAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +44,12 @@ public final class StaffChatCommand extends AbstractPlayerCommand {
|
|||||||
@Nonnull Ref<EntityStore> ref,
|
@Nonnull Ref<EntityStore> ref,
|
||||||
@Nonnull PlayerRef sender,
|
@Nonnull PlayerRef sender,
|
||||||
@Nonnull World world) {
|
@Nonnull World world) {
|
||||||
|
String body = bodyArg.get(ctx);
|
||||||
|
if (body != null && !body.isBlank()) {
|
||||||
|
staffChat.send(sender, body.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
boolean nowOn = toggle.toggle(sender.getUuid());
|
boolean nowOn = toggle.toggle(sender.getUuid());
|
||||||
if (nowOn) {
|
if (nowOn) {
|
||||||
ctx.sendMessage(Message.raw("Staff chat ON. Your chat now broadcasts to staff. Run /sc again to turn off.").color(Color.GREEN));
|
ctx.sendMessage(Message.raw("Staff chat ON. Your chat now broadcasts to staff. Run /sc again to turn off.").color(Color.GREEN));
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import java.sql.PreparedStatement;
|
|||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Types;
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -69,6 +71,19 @@ public final class PlayerSeenRepository {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public List<PlayerSeen> findRecent(int limit) throws SQLException {
|
||||||
|
String sql = "SELECT * FROM " + table + " ORDER BY last_seen DESC LIMIT ?";
|
||||||
|
List<PlayerSeen> out = new ArrayList<>();
|
||||||
|
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setInt(1, limit);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(map(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public Optional<PlayerSeen> findByName(@Nonnull String name) throws SQLException {
|
public Optional<PlayerSeen> findByName(@Nonnull String name) throws SQLException {
|
||||||
String sql = "SELECT * FROM " + table + " WHERE LOWER(last_name) = LOWER(?) ORDER BY last_seen DESC LIMIT 1";
|
String sql = "SELECT * FROM " + table + " WHERE LOWER(last_name) = LOWER(?) ORDER BY last_seen DESC LIMIT 1";
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package net.kewwbec.staff.ui;
|
||||||
|
|
||||||
|
import au.ellie.hyui.builders.PageBuilder;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.staff.model.PlayerSeen;
|
||||||
|
import net.kewwbec.staff.model.Punishment;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merged feed of recent network activity: punishments (issued + revoked) and player presence
|
||||||
|
* changes (joined / left), interleaved by timestamp newest-first.
|
||||||
|
*
|
||||||
|
* Three tabs (All / Punishments / Presence) using native HyUI tabs so toggling is client-side.
|
||||||
|
*/
|
||||||
|
public final class ActivityFeedPage {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
private static final int LIMIT = 50;
|
||||||
|
|
||||||
|
private final PlayerRef playerRef;
|
||||||
|
private final StaffUIContext ctx;
|
||||||
|
private final PageBuilder page;
|
||||||
|
|
||||||
|
public ActivityFeedPage(@Nonnull PlayerRef playerRef, @Nonnull StaffUIContext ctx) {
|
||||||
|
this.playerRef = playerRef;
|
||||||
|
this.ctx = ctx;
|
||||||
|
|
||||||
|
List<Entry> punishEntries = new ArrayList<>();
|
||||||
|
List<Entry> presenceEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (Punishment p : ctx.punishmentRepo.findRecent(LIMIT)) {
|
||||||
|
punishEntries.add(new Entry(
|
||||||
|
p.issuedAt(),
|
||||||
|
"PUNISH",
|
||||||
|
p.type().name() + " " + p.targetName() + " by " + p.issuedByName() + " - " + p.reason()
|
||||||
|
));
|
||||||
|
if (p.revokedAt() != null) {
|
||||||
|
punishEntries.add(new Entry(
|
||||||
|
p.revokedAt(),
|
||||||
|
"REVOKE",
|
||||||
|
"REVOKED " + p.type().name() + " on " + p.targetName()
|
||||||
|
+ " by " + (p.revokedByName() == null ? "?" : p.revokedByName())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Punishment activity query failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (PlayerSeen s : ctx.seenRepo.findRecent(LIMIT)) {
|
||||||
|
presenceEntries.add(new Entry(
|
||||||
|
s.lastSeen(),
|
||||||
|
s.online() ? "JOIN" : "LEAVE",
|
||||||
|
s.lastName() + " " + (s.online() ? "online on " + s.lastServerId() : "offline")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Presence activity query failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Entry> all = new ArrayList<>();
|
||||||
|
all.addAll(punishEntries);
|
||||||
|
all.addAll(presenceEntries);
|
||||||
|
all.sort(Comparator.comparingLong(Entry::ts).reversed());
|
||||||
|
if (all.size() > LIMIT) all = all.subList(0, LIMIT);
|
||||||
|
|
||||||
|
String body = """
|
||||||
|
<p>Last %d events across the network.</p>
|
||||||
|
<nav id="activityTabs" class="tabs"
|
||||||
|
data-tabs="all:All:allContent,punish:Punishments:punishContent,presence:Presence:presenceContent"
|
||||||
|
data-selected="all"></nav>
|
||||||
|
<div id="allContent" class="tab-content" data-hyui-tab-id="all">
|
||||||
|
%s
|
||||||
|
</div>
|
||||||
|
<div id="punishContent" class="tab-content" data-hyui-tab-id="punish">
|
||||||
|
%s
|
||||||
|
</div>
|
||||||
|
<div id="presenceContent" class="tab-content" data-hyui-tab-id="presence">
|
||||||
|
%s
|
||||||
|
</div>
|
||||||
|
<p> </p>
|
||||||
|
<button id="backBtn">Back to Hub</button>
|
||||||
|
""".formatted(LIMIT, renderList(all), renderList(punishEntries), renderList(presenceEntries));
|
||||||
|
|
||||||
|
String html = PageLayout.wrap("Activity Feed", body);
|
||||||
|
|
||||||
|
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||||
|
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||||
|
.fromHtml(html);
|
||||||
|
|
||||||
|
page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||||
|
openInStore(s -> new StaffHubPage(playerRef, ctx).open(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String renderList(@Nonnull List<Entry> entries) {
|
||||||
|
if (entries.isEmpty()) return "<p>No activity recorded.</p>";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (Entry e : entries) {
|
||||||
|
sb.append("<p>[").append(describeAgo(now - e.ts())).append("] ")
|
||||||
|
.append("[").append(e.kind()).append("] ")
|
||||||
|
.append(escape(e.text()))
|
||||||
|
.append("</p>");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String describeAgo(long ms) {
|
||||||
|
if (ms < 0) return "?";
|
||||||
|
Duration d = Duration.ofMillis(ms);
|
||||||
|
if (d.toDays() > 0) return d.toDays() + "d";
|
||||||
|
if (d.toHours() > 0) return d.toHours() + "h";
|
||||||
|
if (d.toMinutes() > 0) return d.toMinutes() + "m";
|
||||||
|
return d.getSeconds() + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(@Nonnull Store<EntityStore> store) {
|
||||||
|
page.open(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openInStore(@Nonnull Consumer<Store<EntityStore>> action) {
|
||||||
|
Ref<EntityStore> ref = playerRef.getReference();
|
||||||
|
if (ref == null) return;
|
||||||
|
action.accept(ref.getStore());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String escape(@Nonnull String s) {
|
||||||
|
return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Entry(long ts, @Nonnull String kind, @Nonnull String text) {}
|
||||||
|
}
|
||||||
@@ -68,16 +68,9 @@ public final class HistoryPage {
|
|||||||
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
|
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
|
||||||
}
|
}
|
||||||
|
|
||||||
String html = """
|
body.append("<p> </p>");
|
||||||
<div class="page-overlay">
|
body.append("<button id=\"backBtn\">Back to Player</button>");
|
||||||
<div class="container" data-hyui-title="History: %s">
|
String html = PageLayout.wrap("History: " + targetName, body.toString());
|
||||||
<div class="container-contents">
|
|
||||||
%s
|
|
||||||
<button id="backBtn">Back</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
""".formatted(escape(targetName), body.toString());
|
|
||||||
|
|
||||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ public final class PlayerDetailPage {
|
|||||||
String unbanBtn = hasActiveBan ? "<button id=\"unbanBtn\">Unban</button>" : "";
|
String unbanBtn = hasActiveBan ? "<button id=\"unbanBtn\">Unban</button>" : "";
|
||||||
String unmuteBtn = hasActiveMute ? "<button id=\"unmuteBtn\">Unmute</button>" : "";
|
String unmuteBtn = hasActiveMute ? "<button id=\"unmuteBtn\">Unmute</button>" : "";
|
||||||
|
|
||||||
String html = """
|
String body = """
|
||||||
<div class="page-overlay">
|
|
||||||
<div class="container" data-hyui-title="%s">
|
|
||||||
<div class="container-contents">
|
|
||||||
<p><b>%s</b></p>
|
<p><b>%s</b></p>
|
||||||
<p>UUID: %s</p>
|
<p>UUID: %s</p>
|
||||||
<p>%s</p>
|
<p>%s</p>
|
||||||
@@ -104,13 +101,11 @@ public final class PlayerDetailPage {
|
|||||||
%s
|
%s
|
||||||
<p> </p>
|
<p> </p>
|
||||||
<p><b>Records</b></p>
|
<p><b>Records</b></p>
|
||||||
<button id="historyBtn">View full history</button>
|
<button id="historyBtn" class="secondary-button">View full history</button>
|
||||||
<p> </p>
|
<p> </p>
|
||||||
<button id="backBtn">Back to Lookup</button>
|
<button id="backBtn">Back to Lookup</button>
|
||||||
</div>
|
""".formatted(escape(targetName), targetUuid, statusLine, lastSeenLine, historyCount, banLine, muteLine, unbanBtn, unmuteBtn);
|
||||||
</div>
|
String html = PageLayout.wrap(targetName, body);
|
||||||
</div>
|
|
||||||
""".formatted(escape(targetName), escape(targetName), targetUuid, statusLine, lastSeenLine, historyCount, banLine, muteLine, unbanBtn, unmuteBtn);
|
|
||||||
|
|
||||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||||
|
|||||||
@@ -49,23 +49,18 @@ public final class PlayerLookupPage {
|
|||||||
? "<p>" + (query.isBlank() ? "Type a name and click Search. Empty search lists everyone online." : "No matches for '" + escape(query) + "'.") + "</p>"
|
? "<p>" + (query.isBlank() ? "Type a name and click Search. Empty search lists everyone online." : "No matches for '" + escape(query) + "'.") + "</p>"
|
||||||
: rows.toString();
|
: rows.toString();
|
||||||
|
|
||||||
String html = """
|
String body = """
|
||||||
<div class="page-overlay">
|
|
||||||
<div class="container" data-hyui-title="Player Lookup">
|
|
||||||
<div class="container-contents">
|
|
||||||
<p>Search by username. Online players first, then offline players from history.</p>
|
<p>Search by username. Online players first, then offline players from history.</p>
|
||||||
<input type="text" id="searchInput" placeholder="Player name" value="%s" />
|
<input type="text" id="searchInput" placeholder="Player name" value="%s" />
|
||||||
<button id="searchBtn">Search</button>
|
<button id="searchBtn">Search</button>
|
||||||
<button id="clearBtn">List all online</button>
|
<button id="clearBtn" class="secondary-button">List all online</button>
|
||||||
<p> </p>
|
<p> </p>
|
||||||
<p><b>Results: %d</b></p>
|
<p><b>Results: %d</b></p>
|
||||||
%s
|
%s
|
||||||
<p> </p>
|
<p> </p>
|
||||||
<button id="backBtn">Back to Hub</button>
|
<button id="backBtn">Back to Hub</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
""".formatted(escape(query), hits.size(), resultsHtml);
|
""".formatted(escape(query), hits.size(), resultsHtml);
|
||||||
|
String html = PageLayout.wrap("Player Lookup", body);
|
||||||
|
|
||||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||||
|
|||||||
@@ -22,14 +22,19 @@ public final class PunishFormPage {
|
|||||||
|
|
||||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
private static final String[] DURATION_PRESETS = { "30m", "1h", "6h", "1d", "3d", "7d", "30d" };
|
private static final String[] DURATION_OPTIONS = {
|
||||||
private static final String[] REASON_PRESETS = {
|
"30m", "1h", "6h", "1d", "3d", "7d", "30d", "custom"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String[] REASON_OPTIONS = {
|
||||||
|
"",
|
||||||
"Cheating",
|
"Cheating",
|
||||||
"Hate speech",
|
"Hate speech",
|
||||||
"Harassment",
|
"Harassment",
|
||||||
"Spam",
|
"Spam",
|
||||||
"Inappropriate name",
|
"Inappropriate name",
|
||||||
"Exploiting bugs"
|
"Exploiting bugs",
|
||||||
|
"Other (type below)"
|
||||||
};
|
};
|
||||||
|
|
||||||
private final PlayerRef playerRef;
|
private final PlayerRef playerRef;
|
||||||
@@ -64,31 +69,37 @@ public final class PunishFormPage {
|
|||||||
StringBuilder durationBlock = new StringBuilder();
|
StringBuilder durationBlock = new StringBuilder();
|
||||||
if (type.isTimed()) {
|
if (type.isTimed()) {
|
||||||
durationBlock.append("<p><b>Duration</b></p>");
|
durationBlock.append("<p><b>Duration</b></p>");
|
||||||
durationBlock.append("<p>Click a preset, or type a custom value (e.g. 30s, 12h, 2w).</p>");
|
durationBlock.append("<p>Pick a preset, or pick 'custom' and type a value (e.g. 12h, 2w).</p>");
|
||||||
for (int i = 0; i < DURATION_PRESETS.length; i++) {
|
durationBlock.append("<select id=\"durationSelect\">");
|
||||||
durationBlock.append("<button id=\"dPreset").append(i).append("\">")
|
for (String d : DURATION_OPTIONS) {
|
||||||
.append(DURATION_PRESETS[i]).append("</button>");
|
boolean selected = matchesPreset(duration, d);
|
||||||
|
durationBlock.append("<option value=\"").append(d).append("\"")
|
||||||
|
.append(selected ? " selected=\"true\"" : "")
|
||||||
|
.append(">").append(d).append("</option>");
|
||||||
}
|
}
|
||||||
durationBlock.append("<input type=\"text\" id=\"durationInput\" placeholder=\"Custom duration\" value=\"")
|
durationBlock.append("</select>");
|
||||||
.append(escape(duration)).append("\" />");
|
durationBlock.append("<input type=\"text\" id=\"durationInput\" placeholder=\"Custom (used when 'custom' selected)\" value=\"")
|
||||||
|
.append(escape(isPreset(duration) ? "" : duration)).append("\" />");
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder reasonPresets = new StringBuilder();
|
StringBuilder reasonBlock = new StringBuilder();
|
||||||
reasonPresets.append("<p><b>Reason</b></p>");
|
reasonBlock.append("<p><b>Reason</b></p>");
|
||||||
reasonPresets.append("<p>Pick a preset to fill the field, or type your own.</p>");
|
reasonBlock.append("<p>Pick a preset, or pick 'Other' and type your own.</p>");
|
||||||
for (int i = 0; i < REASON_PRESETS.length; i++) {
|
reasonBlock.append("<select id=\"reasonSelect\">");
|
||||||
reasonPresets.append("<button id=\"rPreset").append(i).append("\">")
|
for (String r : REASON_OPTIONS) {
|
||||||
.append(escape(REASON_PRESETS[i])).append("</button>");
|
String label = r.isEmpty() ? "-- choose --" : r;
|
||||||
|
boolean selected = !r.isEmpty() && r.equals(reason);
|
||||||
|
reasonBlock.append("<option value=\"").append(escape(r)).append("\"")
|
||||||
|
.append(selected ? " selected=\"true\"" : "")
|
||||||
|
.append(">").append(escape(label)).append("</option>");
|
||||||
}
|
}
|
||||||
reasonPresets.append("<input type=\"text\" id=\"reasonInput\" placeholder=\"Reason\" value=\"")
|
reasonBlock.append("</select>");
|
||||||
.append(escape(reason)).append("\" />");
|
reasonBlock.append("<input type=\"text\" id=\"reasonInput\" placeholder=\"Custom reason (used when 'Other')\" value=\"")
|
||||||
|
.append(escape(isReasonPreset(reason) ? "" : reason)).append("\" />");
|
||||||
|
|
||||||
String errorLine = error.isBlank() ? "" : "<p>! " + escape(error) + "</p>";
|
String errorLine = error.isBlank() ? "" : "<p>! " + escape(error) + "</p>";
|
||||||
|
|
||||||
String html = """
|
String body = """
|
||||||
<div class="page-overlay">
|
|
||||||
<div class="container" data-hyui-title="Issue %s">
|
|
||||||
<div class="container-contents">
|
|
||||||
<p>Target: <b>%s</b></p>
|
<p>Target: <b>%s</b></p>
|
||||||
<p> </p>
|
<p> </p>
|
||||||
%s
|
%s
|
||||||
@@ -97,55 +108,47 @@ public final class PunishFormPage {
|
|||||||
<p> </p>
|
<p> </p>
|
||||||
%s
|
%s
|
||||||
<button id="submitBtn">Confirm %s</button>
|
<button id="submitBtn">Confirm %s</button>
|
||||||
<button id="cancelBtn">Cancel</button>
|
<button id="cancelBtn" class="secondary-button">Cancel</button>
|
||||||
</div>
|
""".formatted(escape(targetName), durationBlock.toString(), reasonBlock.toString(), errorLine, type.name());
|
||||||
</div>
|
|
||||||
</div>
|
String html = PageLayout.wrap("Issue " + type.name(), body);
|
||||||
""".formatted(type.name(), escape(targetName), durationBlock.toString(), reasonPresets.toString(), errorLine, type.name());
|
|
||||||
|
|
||||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||||
.fromHtml(html);
|
.fromHtml(html);
|
||||||
|
|
||||||
if (type.isTimed()) {
|
|
||||||
for (int i = 0; i < DURATION_PRESETS.length; i++) {
|
|
||||||
final String preset = DURATION_PRESETS[i];
|
|
||||||
page.addEventListener("dPreset" + i, CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
|
|
||||||
String r = pCtx.getValue("reasonInput", String.class).orElse("").trim();
|
|
||||||
rerender(preset, r, "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < REASON_PRESETS.length; i++) {
|
|
||||||
final String preset = REASON_PRESETS[i];
|
|
||||||
page.addEventListener("rPreset" + i, CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
|
|
||||||
String d = type.isTimed() ? pCtx.getValue("durationInput", String.class).orElse("").trim() : "";
|
|
||||||
rerender(d, preset, "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
page.addEventListener("submitBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
|
page.addEventListener("submitBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
|
||||||
String r = pCtx.getValue("reasonInput", String.class).orElse("").trim();
|
String reasonPick = pCtx.getValue("reasonSelect", String.class).orElse("");
|
||||||
String d = type.isTimed() ? pCtx.getValue("durationInput", String.class).orElse("").trim() : "";
|
String reasonCustom = pCtx.getValue("reasonInput", String.class).orElse("").trim();
|
||||||
|
String finalReason = (reasonPick.isEmpty() || reasonPick.startsWith("Other"))
|
||||||
|
? reasonCustom
|
||||||
|
: reasonPick;
|
||||||
|
|
||||||
if (r.isEmpty()) {
|
String durPick = type.isTimed()
|
||||||
rerender(d, r, "Reason is required.");
|
? pCtx.getValue("durationSelect", String.class).orElse("")
|
||||||
|
: "";
|
||||||
|
String durCustom = type.isTimed()
|
||||||
|
? pCtx.getValue("durationInput", String.class).orElse("").trim()
|
||||||
|
: "";
|
||||||
|
String finalDuration = "custom".equals(durPick) ? durCustom : durPick;
|
||||||
|
|
||||||
|
if (finalReason.isEmpty()) {
|
||||||
|
rerender(finalDuration, finalReason, "Reason is required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Long durationMs = null;
|
Long durationMs = null;
|
||||||
if (type.isTimed()) {
|
if (type.isTimed()) {
|
||||||
if (d.isEmpty()) {
|
if (finalDuration.isEmpty()) {
|
||||||
rerender(d, r, "Duration is required for " + type.name() + ".");
|
rerender(finalDuration, finalReason, "Duration is required for " + type.name() + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
durationMs = DurationParser.parseMillis(d);
|
durationMs = DurationParser.parseMillis(finalDuration);
|
||||||
if (durationMs == null) {
|
if (durationMs == null) {
|
||||||
rerender(d, r, "Bad duration. Use 30s, 5m, 2h, 7d, 1w.");
|
rerender(finalDuration, finalReason, "Bad duration. Use 30s, 5m, 2h, 7d, 1w.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
submit(r, durationMs);
|
submit(finalReason, durationMs);
|
||||||
});
|
});
|
||||||
|
|
||||||
page.addEventListener("cancelBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
page.addEventListener("cancelBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||||
@@ -182,6 +185,22 @@ public final class PunishFormPage {
|
|||||||
action.accept(ref.getStore());
|
action.accept(ref.getStore());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean matchesPreset(@Nonnull String d, @Nonnull String option) {
|
||||||
|
if (d.isEmpty()) return option.equals("30m");
|
||||||
|
if (option.equals("custom")) return !isPreset(d);
|
||||||
|
return d.equals(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPreset(@Nonnull String d) {
|
||||||
|
for (String p : DURATION_OPTIONS) if (!p.equals("custom") && p.equals(d)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isReasonPreset(@Nonnull String r) {
|
||||||
|
for (String p : REASON_OPTIONS) if (!p.isEmpty() && !p.startsWith("Other") && p.equals(r)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private static String escape(@Nonnull String s) {
|
private static String escape(@Nonnull String s) {
|
||||||
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||||
|
|||||||
@@ -60,16 +60,9 @@ public final class RecentPunishmentsPage {
|
|||||||
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
|
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
|
||||||
}
|
}
|
||||||
|
|
||||||
String html = """
|
body.append("<p> </p>");
|
||||||
<div class="page-overlay">
|
body.append("<button id=\"backBtn\">Back to Hub</button>");
|
||||||
<div class="container" data-hyui-title="Recent Punishments">
|
String html = PageLayout.wrap("Recent Punishments", body.toString());
|
||||||
<div class="container-contents">
|
|
||||||
%s
|
|
||||||
<button id="backBtn">Back</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
""".formatted(body.toString());
|
|
||||||
|
|
||||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ public final class StaffHelpPage {
|
|||||||
this.playerRef = playerRef;
|
this.playerRef = playerRef;
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
|
|
||||||
String html = """
|
String body = """
|
||||||
<div class="page-overlay">
|
|
||||||
<div class="container" data-hyui-title="Staff Commands">
|
|
||||||
<div class="container-contents">
|
|
||||||
<p><b>Punishments</b></p>
|
<p><b>Punishments</b></p>
|
||||||
<p>/ban <player> <reason...></p>
|
<p>/ban <player> <reason...></p>
|
||||||
<p>/tempban <player> <duration> <reason...></p>
|
<p>/tempban <player> <duration> <reason...></p>
|
||||||
@@ -35,18 +32,20 @@ public final class StaffHelpPage {
|
|||||||
<p>/unban <player></p>
|
<p>/unban <player></p>
|
||||||
<p>/unmute <player></p>
|
<p>/unmute <player></p>
|
||||||
<p>Durations: 30s, 5m, 2h, 7d, 1w</p>
|
<p>Durations: 30s, 5m, 2h, 7d, 1w</p>
|
||||||
|
<p> </p>
|
||||||
<p><b>Lookup</b></p>
|
<p><b>Lookup</b></p>
|
||||||
<p>/lookup <player> - active punishments + presence</p>
|
<p>/lookup <player> - active punishments + presence</p>
|
||||||
<p>/history <player> - full punishment history</p>
|
<p>/history <player> - full punishment history</p>
|
||||||
|
<p> </p>
|
||||||
<p><b>Staff chat</b></p>
|
<p><b>Staff chat</b></p>
|
||||||
<p>/sc - toggle staff chat mode (your chat goes to staff)</p>
|
<p>/sc - toggle staff chat mode (your chat goes to staff)</p>
|
||||||
|
<p> </p>
|
||||||
<p><b>UI</b></p>
|
<p><b>UI</b></p>
|
||||||
<p>/staffui - open this hub</p>
|
<p>/staffui - open this hub</p>
|
||||||
|
<p> </p>
|
||||||
<button id="backBtn">Back</button>
|
<button id="backBtn">Back</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
""";
|
""";
|
||||||
|
String html = PageLayout.wrap("Staff Commands", body);
|
||||||
|
|
||||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public final class StaffHubPage {
|
|||||||
<p> </p>
|
<p> </p>
|
||||||
<p><b>History</b></p>
|
<p><b>History</b></p>
|
||||||
<button id="recentBtn">Recent Punishments - last 50 actions network-wide</button>
|
<button id="recentBtn">Recent Punishments - last 50 actions network-wide</button>
|
||||||
|
<button id="activityBtn">Activity Feed - punishments + join/leave timeline</button>
|
||||||
<p> </p>
|
<p> </p>
|
||||||
<p><b>Help</b></p>
|
<p><b>Help</b></p>
|
||||||
<button id="helpBtn">Commands Reference - every staff command</button>
|
<button id="helpBtn">Commands Reference - every staff command</button>
|
||||||
@@ -51,6 +52,9 @@ public final class StaffHubPage {
|
|||||||
page.addEventListener("recentBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
page.addEventListener("recentBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||||
openInStore(s -> new RecentPunishmentsPage(playerRef, ctx).open(s))
|
openInStore(s -> new RecentPunishmentsPage(playerRef, ctx).open(s))
|
||||||
);
|
);
|
||||||
|
page.addEventListener("activityBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||||
|
openInStore(s -> new ActivityFeedPage(playerRef, ctx).open(s))
|
||||||
|
);
|
||||||
page.addEventListener("helpBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
page.addEventListener("helpBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||||
openInStore(s -> new StaffHelpPage(playerRef, ctx).open(s))
|
openInStore(s -> new StaffHelpPage(playerRef, ctx).open(s))
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user