import {EventHandler} from "../Events/EventHandler"; import {Container} from "../Container/Container"; import {AnyMenuItem, MenuAction, MenuItemType, TraversalPath} from "./MenuRenderer.types"; import {randomUUID} from "node:crypto"; import {ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, MessageFlags} from "discord.js"; import {Nullable} from "../types/Nullable"; import {IconCache} from "../Icons/IconCache"; import {MessageActionRowComponentBuilder} from "@discordjs/builders"; import {ComponentInteractionEvent} from "../Events/ComponentInteractionEvent"; import {MenuTraversal} from "./MenuTraversal"; export class MenuRenderer { private readonly menuId: string; private eventId: Nullable; private exitButton: ButtonBuilder; private backButton: ButtonBuilder; constructor( private readonly traversal: MenuTraversal, private readonly eventHandler: EventHandler|null = null, private readonly iconCache: Nullable = null ) { this.eventHandler ??= Container.get(EventHandler.name); this.iconCache ??= Container.get(IconCache.name); this.menuId = randomUUID(); this.exitButton = new ButtonBuilder() .setLabel("Exit") .setStyle(ButtonStyle.Danger) .setCustomId(this.getInteractionId("EXIT")) .setEmoji(this.iconCache?.get("door_open_solid_white") ?? ''); this.backButton = new ButtonBuilder() .setLabel("Back") .setStyle(ButtonStyle.Secondary) .setCustomId(this.getInteractionId("MOVE", "BACK")) .setEmoji(this.iconCache?.get("angle_left_solid") ?? ''); } public async display(interaction: CommandInteraction) { this.eventId = this.eventHandler?.addHandler(ComponentInteractionEvent.name, this.handleUIEvents.bind(this)); await interaction.reply({ content: "", components: this.getActionRows(), embeds: [this.getEmbed()], withResponse: true, flags: MessageFlags.Ephemeral }) } public close() { this.eventHandler?.removeHandler(ComponentInteractionEvent.name, this.eventId ?? ''); } private getActionRows(): ActionRowBuilder[] { const navigation = new ActionRowBuilder() if (!this.traversal.isRoot) { navigation.addComponents(this.backButton); } const rows = [ this.getComponentForMenuItem(this.traversal.currentMenuItem), navigation ]; return rows.filter(row => row.components.length > 0); } private getComponentForMenuItem(menuItem: AnyMenuItem): ActionRowBuilder { if (menuItem.type === MenuItemType.Collection) { const navigation = new ActionRowBuilder(); navigation.setComponents( ...menuItem.children.map(item => new ButtonBuilder() .setLabel(item.label) .setStyle(ButtonStyle.Primary) .setCustomId(this.getInteractionId("MOVE", item.traversalKey)) .setEmoji(this.iconCache?.get(item.type === MenuItemType.Field ? 'pen_solid' : "folder_solid") ?? '') ) ) return navigation } if (menuItem.type === MenuItemType.Field) { const action = menuItem.getActionRowBuilder({ path: this.traversal.path }) action.setCustomId(this.getInteractionId("SET", this.traversal.stringifiedPath)); return new ActionRowBuilder().setComponents([action]); } return new ActionRowBuilder(); } private getEmbed(): EmbedBuilder { const embed = new EmbedBuilder() .setTitle(this.traversal.currentMenuItem.label) .setDescription(this.traversal.currentMenuItem.description ?? '') .setAuthor({ name: "/ " + this.traversal.path.join(' / ') }); if (this.traversal.currentMenuItem.type === MenuItemType.Field) { const currentValue = this.traversal.currentMenuItem.getCurrentValue({ path: this.traversal.path }); embed.addFields({ name: "Current Value", value: currentValue }); } return embed; } private getInteractionId(action: MenuAction, parameter: Nullable = undefined): string { return `${this.menuId};${action};${parameter ?? ''}` } private handleUIEvents(ev: ComponentInteractionEvent) { if (!ev.interaction.customId.startsWith(this.menuId)) { return; } const [, action, parameter ] = ev.interaction.customId.split(';') const menuAction = action; switch (menuAction) { case "MOVE": if (parameter === 'BACK') { this.traversal.travelBack(); break; } this.traversal.travelForward(parameter); break; case "SET": { const value = ev.interaction.isAnySelectMenu() ? ev.interaction.values : [""]; const menuItem = this.traversal.getMenuItem(parameter); if (menuItem.type !== MenuItemType.Field) { break; } menuItem.setValue(value, { path: MenuTraversal.unstringifyTraversalPath(parameter) }) break; } } ev.interaction.update({ components: this.getActionRows(), embeds: [ this.getEmbed() ] }) } }