package dev.streamchat; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import java.util.ArrayList; import java.util.List; public final class StreamChatScreen extends Screen { private static final List TWITCH_EVENTS = List.of("ChatMessage", "Follow", "Sub", "ReSub", "GiftSub", "Cheer", "Raid"); private static final List YOUTUBE_EVENTS = List.of("Message", "SuperChat", "SuperSticker", "MemberMilestone", "NewSponsor", "GiftMembershipReceived"); private static final List TIKTOK_EVENTS = List.of("chat", "gift", "share", "follow", "like", "join", "subscribe"); private final Screen parent; private final StreamChatConfig config; private final ConnectionManager connections; private Platform selectedPlatform = Platform.TWITCH; private EditBox streamerBotUrl; private EditBox streamerBotPassword; private EditBox tikFinityUrl; private EditBox prefix; private EditBox template; private Platform boxPlatform; private final List scrollable = new ArrayList<>(); private final List baseY = new ArrayList<>(); private int scroll; private int contentEnd; public StreamChatScreen(Screen parent, StreamChatConfig config, ConnectionManager connections) { super(Component.literal("Stream Chat Settings")); this.parent = parent; this.config = config; this.connections = connections; } @Override protected void init() { // Window resizes re-run init(); keep unsaved edits instead of reverting to the stored values. if (prefix != null) commitFormatting(); int left = width / 2 - 190; int rowWidth = 380; int buttonWidth = 120; addRenderableWidget(Button.builder(toggleLabel(Platform.TWITCH), button -> { config.twitchEnabled = !config.twitchEnabled; button.setMessage(toggleLabel(Platform.TWITCH)); }).bounds(left, 28, buttonWidth, 20).build()); addRenderableWidget(Button.builder(toggleLabel(Platform.YOUTUBE), button -> { config.youtubeEnabled = !config.youtubeEnabled; button.setMessage(toggleLabel(Platform.YOUTUBE)); }).bounds(left + 130, 28, buttonWidth, 20).build()); addRenderableWidget(Button.builder(toggleLabel(Platform.TIKTOK), button -> { config.tiktokEnabled = !config.tiktokEnabled; button.setMessage(toggleLabel(Platform.TIKTOK)); }).bounds(left + 260, 28, buttonWidth, 20).build()); streamerBotUrl = new EditBox(font, left, 75, rowWidth, 20, Component.literal("Streamer.bot WebSocket URL")); streamerBotUrl.setValue(config.streamerBotUrl); streamerBotUrl.setMaxLength(256); addRenderableWidget(streamerBotUrl); streamerBotPassword = new EditBox(font, left, 105, rowWidth, 20, Component.literal("Streamer.bot password")); streamerBotPassword.setValue(config.streamerBotPassword); streamerBotPassword.setMaxLength(256); streamerBotPassword.setSuggestion("Streamer.bot password (stored in config file)"); addRenderableWidget(streamerBotPassword); tikFinityUrl = new EditBox(font, left, 135, rowWidth, 20, Component.literal("TikFinity WebSocket URL")); tikFinityUrl.setValue(config.tikFinityUrl); tikFinityUrl.setMaxLength(256); addRenderableWidget(tikFinityUrl); addRenderableWidget(Button.builder(Component.literal("Streamer.bot: " + (config.streamerBotEnabled ? "On" : "Off")), button -> { config.streamerBotEnabled = !config.streamerBotEnabled; button.setMessage(Component.literal("Streamer.bot: " + (config.streamerBotEnabled ? "On" : "Off"))); }).bounds(left, 165, 180, 20).build()); addRenderableWidget(Button.builder(Component.literal("TikFinity: " + (config.tikFinityEnabled ? "On" : "Off")), button -> { config.tikFinityEnabled = !config.tikFinityEnabled; button.setMessage(Component.literal("TikFinity: " + (config.tikFinityEnabled ? "On" : "Off"))); }).bounds(left + 200, 165, 180, 20).build()); addRenderableWidget(Button.builder(Component.literal("Twitch"), button -> select(Platform.TWITCH)) .bounds(left, 200, 120, 20).build()); addRenderableWidget(Button.builder(Component.literal("YouTube"), button -> select(Platform.YOUTUBE)) .bounds(left + 130, 200, 120, 20).build()); addRenderableWidget(Button.builder(Component.literal("TikTok"), button -> select(Platform.TIKTOK)) .bounds(left + 260, 200, 120, 20).build()); prefix = new EditBox(font, left, 230, rowWidth, 20, Component.literal("Platform prefix")); prefix.setValue(config.getPrefix(selectedPlatform)); boxPlatform = selectedPlatform; prefix.setMaxLength(128); addRenderableWidget(prefix); template = new EditBox(font, left, 270, rowWidth, 20, Component.literal("Chat message format")); template.setValue(config.getChatFormat(selectedPlatform)); template.setMaxLength(256); addRenderableWidget(template); addRouteButtons(left); captureScrollable(); addRenderableWidget(Button.builder(Component.literal("Toggle connections"), button -> connections.toggleConnections()) .bounds(left, height - 32, 120, 20).build()); addRenderableWidget(Button.builder(Component.literal("Reload config"), button -> reloadConfig()) .bounds(left + 130, height - 32, 120, 20).build()); addRenderableWidget(Button.builder(Component.literal("Done"), button -> onClose()) .bounds(left + 260, height - 32, 120, 20).build()); } @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { renderBackground(graphics); graphics.drawCenteredString(font, title, width / 2, 8, 0xFFFFFF); int left = width / 2 - 190; label(graphics, "Streamer.bot / TikFinity WebSocket settings", left, 60, 0xA0A0A0); label(graphics, "Connection status: " + connections.status(selectedPlatform), left, 188, 0xFFFFFF); label(graphics, "Last event: " + connections.lastEvent(selectedPlatform), left + 180, 188, 0xA0A0A0); label(graphics, "Prefix", left, 220, 0xA0A0A0); label(graphics, "Chat format ({prefix} {username} {message}; &c colors; other events: platform .yml)", left, 256, 0xA0A0A0); label(graphics, "Event destination (click to cycle)", left, 296, 0xA0A0A0); super.render(graphics, mouseX, mouseY, delta); } private void label(GuiGraphics graphics, String text, int x, int baseY, int color) { int y = baseY - scroll; if (y >= viewTop() && y + 9 <= viewBottom()) { graphics.drawString(font, text, x, y, color); } } private int viewTop() { return 24; } private int viewBottom() { return height - 40; } private int maxScroll() { return Math.max(0, contentEnd + 8 - viewBottom()); } private void captureScrollable() { scrollable.clear(); baseY.clear(); for (var child : children()) { if (child instanceof AbstractWidget widget) { scrollable.add(widget); baseY.add(widget.getY()); } } scroll = Math.min(scroll, maxScroll()); applyScroll(); } private void applyScroll() { for (int i = 0; i < scrollable.size(); i++) { AbstractWidget widget = scrollable.get(i); int y = baseY.get(i) - scroll; widget.setY(y); boolean inView = y >= viewTop() && y + widget.getHeight() <= viewBottom(); widget.visible = inView; widget.active = inView; } } private boolean scrollBy(double amount) { int next = (int) Math.max(0, Math.min(maxScroll(), scroll - amount * 20)); if (next == scroll) return false; scroll = next; applyScroll(); return true; } @Override public boolean mouseScrolled(double mouseX, double mouseY, double delta) { return scrollBy(delta) || super.mouseScrolled(mouseX, mouseY, delta); } @Override public void onClose() { commitFormatting(); config.save(); connections.configurationChanged(); minecraft.setScreen(parent); } private void commitFormatting() { config.streamerBotUrl = streamerBotUrl.getValue().trim(); config.streamerBotPassword = streamerBotPassword.getValue(); config.tikFinityUrl = tikFinityUrl.getValue().trim(); config.prefixes.put(boxPlatform.name(), prefix.getValue()); config.setChatFormat(boxPlatform, template.getValue()); } private void select(Platform platform) { commitFormatting(); selectedPlatform = platform; rebuildRoutes(); } private void reloadConfig() { config.reload(); connections.configurationChanged(); prefix = null; rebuildRoutes(); } private void rebuildRoutes() { clearWidgets(); init(); } private void addRouteButtons(int left) { List events = switch (selectedPlatform) { case TWITCH -> TWITCH_EVENTS; case YOUTUBE -> YOUTUBE_EVENTS; case TIKTOK -> TIKTOK_EVENTS; }; contentEnd = 320 + (events.size() + 1) / 2 * 24; for (int i = 0; i < events.size(); i++) { String eventType = events.get(i); int x = left + (i % 2) * 195; int y = 320 + (i / 2) * 24; addRenderableWidget(Button.builder(routeLabel(eventType), button -> { String key = StreamChatConfig.routeKey(selectedPlatform, eventType); DisplayTarget[] targets = DisplayTarget.values(); DisplayTarget current = config.routes.getOrDefault(key, DisplayTarget.HIDDEN); config.routes.put(key, targets[(current.ordinal() + 1) % targets.length]); button.setMessage(routeLabel(eventType)); }).bounds(x, y, 185, 20).build()); } } private Component routeLabel(String eventType) { DisplayTarget target = config.routes.getOrDefault( StreamChatConfig.routeKey(selectedPlatform, eventType), DisplayTarget.HIDDEN); return Component.literal(eventType + ": " + target.name()); } private Component toggleLabel(Platform platform) { return Component.literal(platform.name() + ": " + (config.isEnabled(platform) ? "On" : "Off")); } }