janky ui + /sc <message> exists now

This commit is contained in:
2026-05-27 09:07:32 -04:00
parent e236e1bab3
commit 5e245c1054
11 changed files with 341 additions and 159 deletions
@@ -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>&nbsp;</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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
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>&nbsp;</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>&nbsp;</p> <p>&nbsp;</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>&nbsp;</p> <p>&nbsp;</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>&nbsp;</p> <p>&nbsp;</p>
<p><b>Results: %d</b></p> <p><b>Results: %d</b></p>
%s %s
<p>&nbsp;</p> <p>&nbsp;</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>&nbsp;</p> <p>&nbsp;</p>
%s %s
@@ -97,55 +108,47 @@ public final class PunishFormPage {
<p>&nbsp;</p> <p>&nbsp;</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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;"); return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
@@ -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>&nbsp;</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 &lt;player&gt; &lt;reason...&gt;</p> <p>/ban &lt;player&gt; &lt;reason...&gt;</p>
<p>/tempban &lt;player&gt; &lt;duration&gt; &lt;reason...&gt;</p> <p>/tempban &lt;player&gt; &lt;duration&gt; &lt;reason...&gt;</p>
@@ -35,18 +32,20 @@ public final class StaffHelpPage {
<p>/unban &lt;player&gt;</p> <p>/unban &lt;player&gt;</p>
<p>/unmute &lt;player&gt;</p> <p>/unmute &lt;player&gt;</p>
<p>Durations: 30s, 5m, 2h, 7d, 1w</p> <p>Durations: 30s, 5m, 2h, 7d, 1w</p>
<p>&nbsp;</p>
<p><b>Lookup</b></p> <p><b>Lookup</b></p>
<p>/lookup &lt;player&gt; - active punishments + presence</p> <p>/lookup &lt;player&gt; - active punishments + presence</p>
<p>/history &lt;player&gt; - full punishment history</p> <p>/history &lt;player&gt; - full punishment history</p>
<p>&nbsp;</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>&nbsp;</p>
<p><b>UI</b></p> <p><b>UI</b></p>
<p>/staffui - open this hub</p> <p>/staffui - open this hub</p>
<p>&nbsp;</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>&nbsp;</p> <p>&nbsp;</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>&nbsp;</p> <p>&nbsp;</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))
); );