pnp-scheduler/source/Menu/MenuRenderer.ts

213 lines
No EOL
6.8 KiB
TypeScript

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,
MessageComponentInteraction,
MessageFlags
} from "discord.js";
import {Nullable} from "../types/Nullable";
import {IconCache} from "../Icons/IconCache";
import {MessageActionRowComponentBuilder} from "@discordjs/builders";
import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent";
import {MenuTraversal} from "./MenuTraversal";
import {Prompt} from "./Modals/Prompt";
import _ from "lodash";
import {EmbedLibrary} from "../Discord/EmbedLibrary";
export class MenuRenderer {
private readonly menuId: string;
private eventId: Nullable<string>;
private backButton: ButtonBuilder;
private static MAX_BUTTON_PER_ROW = 5;
private static MAX_ROWS = 5;
private static SYSTEM_ROW_COUNT = 1;
private static MAX_USER_ROW_COUNT = MenuRenderer.MAX_ROWS - MenuRenderer.SYSTEM_ROW_COUNT;
constructor(
private readonly traversal: MenuTraversal,
private readonly eventHandler: EventHandler|null = null,
private readonly iconCache: Nullable<IconCache> = null,
private readonly rootName: string = '',
) {
this.eventHandler ??= Container.get<EventHandler>(EventHandler.name);
this.iconCache ??= Container.get<IconCache>(IconCache.name);
this.menuId = randomUUID();
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>(ComponentInteractionEvent.name, {
method: 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<MessageActionRowComponentBuilder>[] {
const navigation = new ActionRowBuilder<ButtonBuilder>()
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<MessageActionRowComponentBuilder>[] {
if (menuItem.type === MenuItemType.Collection) {
const rows = _.chunk<AnyMenuItem>(menuItem.children, MenuRenderer.MAX_BUTTON_PER_ROW);
if (rows.length > MenuRenderer.MAX_USER_ROW_COUNT) {
throw new TypeError(
`A collection can only have a max of ${MenuRenderer.MAX_USER_ROW_COUNT * MenuRenderer.MAX_BUTTON_PER_ROW} entries!`
);
}
return rows
.map((items) => new ActionRowBuilder<ButtonBuilder>()
.setComponents(
...items.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") ?? '')
)
)
)
}
if (menuItem.type === MenuItemType.Field) {
const action = menuItem.getActionRowBuilder({
path: this.traversal.path
})
action.setCustomId(this.getInteractionId("SET", this.traversal.stringifiedPath));
return [new ActionRowBuilder<MessageActionRowComponentBuilder>().setComponents([action])];
}
return [new ActionRowBuilder()];
}
private getEmbed(): EmbedBuilder {
const embed = EmbedLibrary.base(
this.traversal.currentMenuItem.label,
this.traversal.currentMenuItem.description ?? '',
).setFooter({
text: this.rootName + " / " + this.traversal.getTraversedLabels().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<string> = undefined): string {
return `${this.menuId};${action};${parameter ?? ''}`
}
private async handleUIEvents(ev: ComponentInteractionEvent) {
if (!ev.interaction.customId.startsWith(this.menuId)) {
return;
}
ev.acknowledge();
const [, action, parameter ] = ev.interaction.customId.split(';')
const menuAction = <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;
}
}
let updateInteraction: unknown = ev.interaction;
if (this.traversal.currentMenuItem.type === MenuItemType.Prompt) {
const prompt = new Prompt(<EventHandler>this.eventHandler);
const currentValue = this.traversal.currentMenuItem;
const currentValuePath = [...this.traversal.path];
const row = currentValue.getActionRowBuilder({
path: currentValuePath
});
row.setLabel(currentValue.label);
row.setValue(currentValue.getCurrentValue({path: currentValuePath}));
row.setPlaceholder(currentValue.description ?? '');
this.traversal.travelBack();
const value = await prompt.requestValue(
this.traversal.currentMenuItem.label,
row,
ev.interaction
);
currentValue.setValue(
value.value,
{
path: currentValuePath
}
)
updateInteraction = value.interaction;
}
updateInteraction.update({
components: this.getActionRows(),
embeds: [ this.getEmbed() ]
})
}
}