@kyvrixon/utils 1.0.12 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyvrixon/utils",
|
|
3
3
|
"main": "./src/index.ts",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"engines": {
|
|
12
12
|
"bun": "1.3.11"
|
|
13
13
|
},
|
|
14
|
+
"engineStrict": true,
|
|
14
15
|
"repository": {
|
|
15
16
|
"url": "https://github.com/kyvrixon/utils"
|
|
16
17
|
},
|
|
@@ -20,18 +21,22 @@
|
|
|
20
21
|
},
|
|
21
22
|
"types": "./src/index.ts",
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@biomejs/biome": "2.4.
|
|
24
|
+
"@biomejs/biome": "2.4.10",
|
|
24
25
|
"@types/bun": "1.3.11",
|
|
25
26
|
"typescript": "6.0.2"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"chalk": "5.6.2",
|
|
29
|
-
"discord.js": "14.
|
|
30
|
+
"discord.js": "14.26.0"
|
|
30
31
|
},
|
|
31
32
|
"exports": {
|
|
32
33
|
".": {
|
|
33
34
|
"types": "./src/index.ts",
|
|
34
35
|
"import": "./src/index.ts"
|
|
36
|
+
},
|
|
37
|
+
"./*": {
|
|
38
|
+
"types": "./src/lib/*/index.ts",
|
|
39
|
+
"import": "./src/lib/*/index.ts"
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
}
|
|
@@ -7,11 +7,21 @@ import type {
|
|
|
7
7
|
SlashCommandSubcommandsOnlyBuilder,
|
|
8
8
|
} from "discord.js";
|
|
9
9
|
|
|
10
|
+
export interface DiscordCommandMetadata extends Record<string, unknown> {}
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Wraps a discord.js slash command with typed `execute` and optional `autocomplete` handlers.
|
|
12
|
-
* @typeParam C - The bot's `Client` type.
|
|
14
|
+
* @typeParam C - The bot's `Client` type. Inferred from args[0] in `method`.
|
|
15
|
+
*
|
|
16
|
+
* @example To extend the `metadata` types
|
|
17
|
+
* declare module "@kyvrixon/utils" {
|
|
18
|
+
* interface DiscordCommandMetadata {
|
|
19
|
+
* cooldown?: number;
|
|
20
|
+
* category?: string;
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
13
23
|
*/
|
|
14
|
-
export class DiscordCommand<C extends Client
|
|
24
|
+
export class DiscordCommand<C extends Client> {
|
|
15
25
|
public readonly data:
|
|
16
26
|
| SlashCommandBuilder
|
|
17
27
|
| SlashCommandOptionsOnlyBuilder
|
|
@@ -24,13 +34,16 @@ export class DiscordCommand<C extends Client<boolean>> {
|
|
|
24
34
|
client: C,
|
|
25
35
|
interaction: AutocompleteInteraction,
|
|
26
36
|
) => Promise<void>;
|
|
37
|
+
public metadata: DiscordCommandMetadata;
|
|
27
38
|
|
|
28
39
|
constructor(ops: {
|
|
29
40
|
data: DiscordCommand<C>["data"];
|
|
41
|
+
metadata: DiscordCommand<C>["metadata"];
|
|
30
42
|
execute: DiscordCommand<C>["execute"];
|
|
31
43
|
autocomplete?: DiscordCommand<C>["autocomplete"];
|
|
32
44
|
}) {
|
|
33
45
|
this.data = ops.data;
|
|
46
|
+
this.metadata = ops.metadata;
|
|
34
47
|
this.execute = ops.execute;
|
|
35
48
|
this.autocomplete = ops.autocomplete;
|
|
36
49
|
}
|
|
@@ -10,32 +10,29 @@ import type { Client, ClientEvents, RestEvents } from "discord.js";
|
|
|
10
10
|
* }
|
|
11
11
|
* }
|
|
12
12
|
*/
|
|
13
|
-
// biome-ignore lint/suspicious/noEmptyInterface:
|
|
13
|
+
// biome-ignore lint/suspicious/noEmptyInterface: It's fine
|
|
14
14
|
export interface DiscordEventCustomType {}
|
|
15
15
|
|
|
16
16
|
/** Maps an event type discriminant to its corresponding event map. */
|
|
17
|
-
type EventMap<T extends "client" | "rest" | "custom"> =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
export type EventMap<T extends "client" | "rest" | "custom"> =
|
|
18
|
+
T extends "client"
|
|
19
|
+
? ClientEvents
|
|
20
|
+
: T extends "rest"
|
|
21
|
+
? RestEvents
|
|
22
|
+
: keyof DiscordEventCustomType extends never
|
|
23
|
+
? { "sooo.. there's no custom events..": [] }
|
|
24
|
+
: DiscordEventCustomType;
|
|
24
25
|
|
|
25
26
|
/** Resolves the argument tuple for a given event type + key pair. */
|
|
26
|
-
type EventArgs<
|
|
27
|
+
export type EventArgs<
|
|
27
28
|
T extends "client" | "rest" | "custom",
|
|
28
29
|
K extends keyof EventMap<T>,
|
|
29
30
|
> = Extract<EventMap<T>[K], any[]>;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Wraps a discord.js event handler. Supports `"client"`, `"rest"`, and `"custom"` event types.
|
|
33
|
-
* @typeParam V - The bot's `Client` type.
|
|
34
|
-
* @typeParam T - The event source discriminant.
|
|
35
|
-
* @typeParam K - The event name within the chosen source.
|
|
36
34
|
*/
|
|
37
35
|
export class DiscordEvent<
|
|
38
|
-
V extends Client,
|
|
39
36
|
T extends "client" | "rest" | "custom",
|
|
40
37
|
K extends keyof EventMap<T> = keyof EventMap<T>,
|
|
41
38
|
> {
|
|
@@ -43,7 +40,7 @@ export class DiscordEvent<
|
|
|
43
40
|
public readonly name: K;
|
|
44
41
|
public readonly once: boolean;
|
|
45
42
|
public readonly method: (
|
|
46
|
-
client:
|
|
43
|
+
client: Client<true>,
|
|
47
44
|
...args: EventArgs<T, K>
|
|
48
45
|
) => void | Promise<void>;
|
|
49
46
|
|
|
@@ -51,7 +48,7 @@ export class DiscordEvent<
|
|
|
51
48
|
type: T;
|
|
52
49
|
name: K;
|
|
53
50
|
once: boolean;
|
|
54
|
-
method: DiscordEvent<
|
|
51
|
+
method: DiscordEvent<T, K>["method"];
|
|
55
52
|
}) {
|
|
56
53
|
this.type = opts.type;
|
|
57
54
|
this.name = opts.name;
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
type ChatInputCommandInteraction,
|
|
9
9
|
ComponentType,
|
|
10
10
|
ContainerBuilder,
|
|
11
|
+
EmbedBuilder,
|
|
11
12
|
FileBuilder,
|
|
12
13
|
LabelBuilder,
|
|
13
14
|
MediaGalleryBuilder,
|
|
14
15
|
type MentionableSelectMenuBuilder,
|
|
16
|
+
type Message,
|
|
15
17
|
ModalBuilder,
|
|
16
18
|
type RoleSelectMenuBuilder,
|
|
17
19
|
SectionBuilder,
|
|
@@ -33,8 +35,8 @@ export type MessageActionRow = ActionRowBuilder<
|
|
|
33
35
|
| MentionableSelectMenuBuilder
|
|
34
36
|
>;
|
|
35
37
|
|
|
36
|
-
const BUTTONS_SYMBOL: unique symbol = Symbol("pagination-buttons");
|
|
37
|
-
const DATA_SYMBOL: unique symbol = Symbol("pagination-data");
|
|
38
|
+
export const BUTTONS_SYMBOL: unique symbol = Symbol("pagination-buttons");
|
|
39
|
+
export const DATA_SYMBOL: unique symbol = Symbol("pagination-data");
|
|
38
40
|
|
|
39
41
|
/** Valid component types that can appear in a pagination page layout. */
|
|
40
42
|
export type PaginationInput =
|
|
@@ -58,42 +60,76 @@ type InternalComponent =
|
|
|
58
60
|
| { type: "gallery"; component: MediaGalleryBuilder }
|
|
59
61
|
| { type: "actionrow"; component: MessageActionRow };
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
/** Shared options for all pagination modes. */
|
|
64
|
+
export interface PaginationBaseOptions {
|
|
62
65
|
/** Number of list entries shown per page (default: 5). */
|
|
63
66
|
entriesPerPage?: number;
|
|
64
67
|
/** Key-value pairs replaced in rendered content. */
|
|
65
68
|
replacements?: Record<string, string>;
|
|
66
|
-
/** Per-page container styling overrides. */
|
|
67
|
-
styling?: Array<{ accent_color?: number; spoiler?: boolean }>;
|
|
68
69
|
/** Whether the pagination message is ephemeral. */
|
|
69
70
|
ephemeral?: boolean;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
74
|
+
* Options for **container** mode (Components V2).
|
|
75
|
+
* Uses a `ContainerBuilder`-based layout with the `IsComponentsV2` message flag.
|
|
76
|
+
*/
|
|
77
|
+
export interface PaginationContainerOptions extends PaginationBaseOptions {
|
|
78
|
+
/** Selects container mode. */
|
|
79
|
+
type: "container";
|
|
80
|
+
/** Single layout template using sentinels `DiscordPagination.DATA` and `DiscordPagination.BUTTONS`. */
|
|
81
|
+
layout: PaginationInput[];
|
|
82
|
+
/** Container accent color. */
|
|
83
|
+
accentColor?: number;
|
|
84
|
+
/** Whether the container is a spoiler. */
|
|
85
|
+
spoiler?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Options for **embed** mode.
|
|
90
|
+
* Uses a standard `EmbedBuilder` with an `ActionRow` for navigation buttons.
|
|
91
|
+
* The embed's `description` and `footer` are reserved for page data and the page counter.
|
|
92
|
+
*/
|
|
93
|
+
export interface PaginationEmbedOptions extends PaginationBaseOptions {
|
|
94
|
+
/** Selects embed mode. */
|
|
95
|
+
type: "embed";
|
|
96
|
+
/** EmbedBuilder template. Description and footer are overwritten per page. */
|
|
97
|
+
embed: EmbedBuilder;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Discriminated union of all pagination option types. Use the `type` field to select a mode. */
|
|
101
|
+
export type PaginationOptions =
|
|
102
|
+
| PaginationContainerOptions
|
|
103
|
+
| PaginationEmbedOptions;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Discord paginator supporting both **Components V2** (`ContainerBuilder`) and
|
|
107
|
+
* **Embed** (`EmbedBuilder`) modes.
|
|
75
108
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
109
|
+
* @example Container mode
|
|
110
|
+
* ```ts
|
|
111
|
+
* const pagination = new DiscordPagination(entries, {
|
|
112
|
+
* type: "container",
|
|
113
|
+
* layout: [
|
|
114
|
+
* "# Leaderboard",
|
|
115
|
+
* new SeparatorBuilder(),
|
|
116
|
+
* DiscordPagination.DATA,
|
|
117
|
+
* new SeparatorBuilder(),
|
|
118
|
+
* DiscordPagination.BUTTONS,
|
|
119
|
+
* ],
|
|
120
|
+
* entriesPerPage: 5,
|
|
121
|
+
* accentColor: 0x5865f2,
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
78
124
|
*
|
|
79
|
-
* @example
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* new SeparatorBuilder(),
|
|
88
|
-
* DiscordPagination.BUTTONS,
|
|
89
|
-
* ],
|
|
90
|
-
* ],
|
|
91
|
-
* {
|
|
92
|
-
* entriesPerPage: 5,
|
|
93
|
-
* ephemeral: false,
|
|
94
|
-
* styling: [{ accent_color: 0x5865f2 }],
|
|
95
|
-
* },
|
|
96
|
-
* );
|
|
125
|
+
* @example Embed mode
|
|
126
|
+
* ```ts
|
|
127
|
+
* const pagination = new DiscordPagination(entries, {
|
|
128
|
+
* type: "embed",
|
|
129
|
+
* embed: new EmbedBuilder().setTitle("Leaderboard").setColor(0x5865f2),
|
|
130
|
+
* entriesPerPage: 5,
|
|
131
|
+
* });
|
|
132
|
+
* ```
|
|
97
133
|
*/
|
|
98
134
|
export class DiscordPagination {
|
|
99
135
|
/** Sentinel — marks where the pagination buttons should render. */
|
|
@@ -102,29 +138,30 @@ export class DiscordPagination {
|
|
|
102
138
|
static readonly DATA: typeof DATA_SYMBOL = DATA_SYMBOL;
|
|
103
139
|
|
|
104
140
|
private readonly list: string[];
|
|
105
|
-
private readonly structure: Array<Array<InternalComponent>>;
|
|
106
141
|
private readonly entriesPerPage: number;
|
|
107
142
|
private readonly replacements?: Record<string, string>;
|
|
108
|
-
private readonly styling?: PaginationOptions["styling"];
|
|
109
143
|
private readonly ephemeral: boolean;
|
|
110
144
|
private readonly prefix: string;
|
|
111
145
|
private readonly totalPages: number;
|
|
146
|
+
private readonly mode: "container" | "embed";
|
|
112
147
|
|
|
148
|
+
// Container mode
|
|
149
|
+
private readonly layout?: InternalComponent[];
|
|
150
|
+
private readonly accentColor?: number;
|
|
151
|
+
private readonly spoiler?: boolean;
|
|
152
|
+
|
|
153
|
+
// Embed mode
|
|
154
|
+
private readonly embedTemplate?: EmbedBuilder;
|
|
155
|
+
|
|
156
|
+
// Runtime state
|
|
113
157
|
private currentIndex = 0;
|
|
114
158
|
private ended = false;
|
|
115
|
-
private
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
) {
|
|
122
|
-
const {
|
|
123
|
-
entriesPerPage = 5,
|
|
124
|
-
replacements,
|
|
125
|
-
styling,
|
|
126
|
-
ephemeral = false,
|
|
127
|
-
} = options;
|
|
159
|
+
private isMessage = false;
|
|
160
|
+
private interaction?: ButtonInteraction | ChatInputCommandInteraction;
|
|
161
|
+
private replyMessage?: Message;
|
|
162
|
+
|
|
163
|
+
constructor(list: string[], options: PaginationOptions) {
|
|
164
|
+
const { entriesPerPage = 5, replacements, ephemeral = false } = options;
|
|
128
165
|
|
|
129
166
|
if (entriesPerPage <= 0)
|
|
130
167
|
throw new Error("entriesPerPage must be greater than 0");
|
|
@@ -132,64 +169,74 @@ export class DiscordPagination {
|
|
|
132
169
|
this.list = list;
|
|
133
170
|
this.entriesPerPage = entriesPerPage;
|
|
134
171
|
this.replacements = replacements;
|
|
135
|
-
this.styling = styling;
|
|
136
172
|
this.ephemeral = ephemeral;
|
|
137
173
|
this.prefix = `~PAGINATION_${randomUUIDv7()}_`;
|
|
138
174
|
this.totalPages = Math.ceil(list.length / entriesPerPage);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
|
|
176
|
+
this.mode = options.type;
|
|
177
|
+
|
|
178
|
+
if (options.type === "container") {
|
|
179
|
+
this.layout = options.layout.map((input) => this.normalize(input));
|
|
180
|
+
this.accentColor = options.accentColor;
|
|
181
|
+
this.spoiler = options.spoiler;
|
|
182
|
+
} else {
|
|
183
|
+
this.embedTemplate = options.embed;
|
|
184
|
+
}
|
|
142
185
|
}
|
|
143
186
|
|
|
144
187
|
/**
|
|
145
188
|
* Sends the paginated message and starts the button collector.
|
|
146
|
-
* @param
|
|
189
|
+
* @param target - The interaction or message to reply to.
|
|
147
190
|
*/
|
|
148
191
|
public async send(
|
|
149
|
-
|
|
192
|
+
target: ButtonInteraction | ChatInputCommandInteraction | Message,
|
|
150
193
|
): Promise<void> {
|
|
151
|
-
this.
|
|
194
|
+
this.isMessage = !("deferReply" in target);
|
|
195
|
+
const userId = this.isMessage
|
|
196
|
+
? (target as Message).author.id
|
|
197
|
+
: (target as ButtonInteraction | ChatInputCommandInteraction).user.id;
|
|
152
198
|
|
|
153
199
|
if (!this.list.length) {
|
|
154
|
-
await
|
|
155
|
-
.reply({
|
|
156
|
-
allowedMentions: { parse: [], repliedUser: false },
|
|
157
|
-
components: [
|
|
158
|
-
new ContainerBuilder().addTextDisplayComponents(
|
|
159
|
-
new TextDisplayBuilder().setContent("No data to show"),
|
|
160
|
-
),
|
|
161
|
-
],
|
|
162
|
-
flags: this.ephemeral
|
|
163
|
-
? ["Ephemeral", "IsComponentsV2"]
|
|
164
|
-
: ["IsComponentsV2"],
|
|
165
|
-
})
|
|
166
|
-
.catch(() => {});
|
|
200
|
+
await this.sendEmpty(target);
|
|
167
201
|
return;
|
|
168
202
|
}
|
|
169
203
|
|
|
170
|
-
if (
|
|
171
|
-
await
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
if (this.isMessage) {
|
|
205
|
+
this.replyMessage = await (target as Message).reply(this.buildPayload());
|
|
206
|
+
} else {
|
|
207
|
+
const interaction = target as
|
|
208
|
+
| ButtonInteraction
|
|
209
|
+
| ChatInputCommandInteraction;
|
|
210
|
+
this.interaction = interaction;
|
|
211
|
+
|
|
212
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
213
|
+
const response = await interaction
|
|
214
|
+
.deferReply({
|
|
215
|
+
withResponse: true,
|
|
216
|
+
flags: this.ephemeral ? ["Ephemeral"] : [],
|
|
217
|
+
})
|
|
218
|
+
.catch(() => null);
|
|
219
|
+
this.replyMessage =
|
|
220
|
+
response?.resource?.message ??
|
|
221
|
+
(await interaction.fetchReply().catch(() => undefined));
|
|
222
|
+
} else {
|
|
223
|
+
this.replyMessage = await interaction
|
|
224
|
+
.fetchReply()
|
|
225
|
+
.catch(() => undefined);
|
|
226
|
+
}
|
|
178
227
|
|
|
179
|
-
|
|
180
|
-
if (!channel || !("createMessageComponentCollector" in channel)) {
|
|
181
|
-
throw new Error("Invalid channel type");
|
|
228
|
+
await this.render();
|
|
182
229
|
}
|
|
183
230
|
|
|
184
|
-
|
|
231
|
+
if (!this.replyMessage) return;
|
|
185
232
|
|
|
186
|
-
const collector =
|
|
233
|
+
const collector = this.replyMessage.createMessageComponentCollector({
|
|
187
234
|
componentType: ComponentType.Button,
|
|
188
|
-
time:
|
|
235
|
+
time: 60_000,
|
|
189
236
|
});
|
|
190
237
|
|
|
191
238
|
collector.on("collect", async (btn) => {
|
|
192
|
-
if (btn.user.id !==
|
|
239
|
+
if (btn.user.id !== userId) {
|
|
193
240
|
return void btn.deferUpdate();
|
|
194
241
|
}
|
|
195
242
|
if (!btn.customId.startsWith(this.prefix)) return;
|
|
@@ -223,6 +270,66 @@ export class DiscordPagination {
|
|
|
223
270
|
});
|
|
224
271
|
}
|
|
225
272
|
|
|
273
|
+
private async sendEmpty(
|
|
274
|
+
target: ButtonInteraction | ChatInputCommandInteraction | Message,
|
|
275
|
+
): Promise<void> {
|
|
276
|
+
if (this.mode === "embed" && !this.embedTemplate)
|
|
277
|
+
throw new Error(
|
|
278
|
+
"[@kyvrixon/utils]: Pagination: embedTemplate is in a corrupted state",
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const allowedMentions = { parse: [] as const, repliedUser: false };
|
|
282
|
+
|
|
283
|
+
if (this.isMessage) {
|
|
284
|
+
const payload =
|
|
285
|
+
this.mode === "container"
|
|
286
|
+
? {
|
|
287
|
+
components: [
|
|
288
|
+
new ContainerBuilder().addTextDisplayComponents(
|
|
289
|
+
new TextDisplayBuilder().setContent("No data to show"),
|
|
290
|
+
),
|
|
291
|
+
],
|
|
292
|
+
flags: ["IsComponentsV2"] as const,
|
|
293
|
+
allowedMentions,
|
|
294
|
+
}
|
|
295
|
+
: {
|
|
296
|
+
embeds: [
|
|
297
|
+
new EmbedBuilder(this.embedTemplate?.toJSON()).setDescription(
|
|
298
|
+
"No data to show",
|
|
299
|
+
),
|
|
300
|
+
],
|
|
301
|
+
allowedMentions,
|
|
302
|
+
};
|
|
303
|
+
await (target as Message).reply(payload).catch(() => {});
|
|
304
|
+
} else {
|
|
305
|
+
const payload =
|
|
306
|
+
this.mode === "container"
|
|
307
|
+
? {
|
|
308
|
+
components: [
|
|
309
|
+
new ContainerBuilder().addTextDisplayComponents(
|
|
310
|
+
new TextDisplayBuilder().setContent("No data to show"),
|
|
311
|
+
),
|
|
312
|
+
],
|
|
313
|
+
flags: this.ephemeral
|
|
314
|
+
? (["Ephemeral", "IsComponentsV2"] as const)
|
|
315
|
+
: (["IsComponentsV2"] as const),
|
|
316
|
+
allowedMentions,
|
|
317
|
+
}
|
|
318
|
+
: {
|
|
319
|
+
embeds: [
|
|
320
|
+
new EmbedBuilder(this.embedTemplate?.toJSON()).setDescription(
|
|
321
|
+
"No data to show",
|
|
322
|
+
),
|
|
323
|
+
],
|
|
324
|
+
flags: this.ephemeral ? (["Ephemeral"] as const) : ([] as const),
|
|
325
|
+
allowedMentions,
|
|
326
|
+
};
|
|
327
|
+
await (target as ButtonInteraction | ChatInputCommandInteraction)
|
|
328
|
+
.reply(payload)
|
|
329
|
+
.catch(() => {});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
226
333
|
private normalize(input: PaginationInput): InternalComponent {
|
|
227
334
|
if (input === BUTTONS_SYMBOL) return { type: "buttons" };
|
|
228
335
|
if (input === DATA_SYMBOL) return { type: "data" };
|
|
@@ -243,27 +350,95 @@ export class DiscordPagination {
|
|
|
243
350
|
return { type: "actionrow", component: input };
|
|
244
351
|
}
|
|
245
352
|
|
|
246
|
-
private
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
structure.push(this.clonePage(lastPage));
|
|
353
|
+
private buildPayload() {
|
|
354
|
+
if (this.mode === "embed") {
|
|
355
|
+
return {
|
|
356
|
+
embeds: [this.generateEmbed()],
|
|
357
|
+
components: [this.getPaginationRow()],
|
|
358
|
+
allowedMentions: { parse: [] as const, repliedUser: false },
|
|
359
|
+
};
|
|
254
360
|
}
|
|
255
361
|
|
|
256
|
-
return
|
|
362
|
+
return {
|
|
363
|
+
components: [this.generateContainer()],
|
|
364
|
+
flags: ["IsComponentsV2"] as const,
|
|
365
|
+
allowedMentions: { parse: [] as const, repliedUser: false },
|
|
366
|
+
};
|
|
257
367
|
}
|
|
258
368
|
|
|
259
|
-
private
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
369
|
+
private generateContainer(): ContainerBuilder {
|
|
370
|
+
if (!this.layout)
|
|
371
|
+
throw new Error(
|
|
372
|
+
"[@kyvrixon/utils]: Pagination: layout is in a corrupted state",
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const page = Math.floor(this.currentIndex / this.entriesPerPage);
|
|
376
|
+
|
|
377
|
+
return new ContainerBuilder({
|
|
378
|
+
components: this.layout.map((comp) => {
|
|
379
|
+
switch (comp.type) {
|
|
380
|
+
case "buttons":
|
|
381
|
+
return this.getPaginationRow().toJSON();
|
|
382
|
+
|
|
383
|
+
case "data": {
|
|
384
|
+
const content = this.list
|
|
385
|
+
.slice(
|
|
386
|
+
page * this.entriesPerPage,
|
|
387
|
+
(page + 1) * this.entriesPerPage,
|
|
388
|
+
)
|
|
389
|
+
.join("\n");
|
|
390
|
+
return new TextDisplayBuilder()
|
|
391
|
+
.setContent(this.applyReplacements(content))
|
|
392
|
+
.toJSON();
|
|
265
393
|
}
|
|
266
|
-
|
|
394
|
+
|
|
395
|
+
default:
|
|
396
|
+
return comp.component.toJSON();
|
|
397
|
+
}
|
|
398
|
+
}),
|
|
399
|
+
accent_color: this.accentColor,
|
|
400
|
+
spoiler: this.spoiler,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private generateEmbed(): EmbedBuilder {
|
|
405
|
+
if (!this.embedTemplate)
|
|
406
|
+
throw new Error(
|
|
407
|
+
"[@kyvrixon/utils]: Pagination: embedTemplate is in a corrupted state",
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const page = Math.floor(this.currentIndex / this.entriesPerPage);
|
|
411
|
+
const content = this.list
|
|
412
|
+
.slice(page * this.entriesPerPage, (page + 1) * this.entriesPerPage)
|
|
413
|
+
.join("\n");
|
|
414
|
+
|
|
415
|
+
return new EmbedBuilder(this.embedTemplate.toJSON())
|
|
416
|
+
.setDescription(this.applyReplacements(content))
|
|
417
|
+
.setFooter({ text: `Page ${page + 1}/${this.totalPages}` });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private getPaginationRow(): ActionRowBuilder<ButtonBuilder> {
|
|
421
|
+
return new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
422
|
+
new ButtonBuilder()
|
|
423
|
+
.setCustomId(`${this.prefix}back`)
|
|
424
|
+
.setLabel("Prev")
|
|
425
|
+
.setStyle(ButtonStyle.Secondary)
|
|
426
|
+
.setDisabled(this.ended || this.currentIndex === 0),
|
|
427
|
+
new ButtonBuilder()
|
|
428
|
+
.setCustomId(`${this.prefix}info`)
|
|
429
|
+
.setLabel(
|
|
430
|
+
`${Math.floor(this.currentIndex / this.entriesPerPage) + 1}/${this.totalPages}`,
|
|
431
|
+
)
|
|
432
|
+
.setStyle(ButtonStyle.Secondary)
|
|
433
|
+
.setDisabled(this.ended || this.totalPages === 1 || this.isMessage),
|
|
434
|
+
new ButtonBuilder()
|
|
435
|
+
.setCustomId(`${this.prefix}forward`)
|
|
436
|
+
.setLabel("Next")
|
|
437
|
+
.setStyle(ButtonStyle.Secondary)
|
|
438
|
+
.setDisabled(
|
|
439
|
+
this.ended ||
|
|
440
|
+
this.currentIndex + this.entriesPerPage >= this.list.length,
|
|
441
|
+
),
|
|
267
442
|
);
|
|
268
443
|
}
|
|
269
444
|
|
|
@@ -321,69 +496,6 @@ export class DiscordPagination {
|
|
|
321
496
|
this.currentIndex = (pageNumber - 1) * this.entriesPerPage;
|
|
322
497
|
}
|
|
323
498
|
|
|
324
|
-
private generateContainer(page: number): ContainerBuilder {
|
|
325
|
-
const pageStructure = this.structure[page];
|
|
326
|
-
if (!pageStructure) throw new Error(`Page ${page} structure not found`);
|
|
327
|
-
|
|
328
|
-
return new ContainerBuilder({
|
|
329
|
-
components: pageStructure.map((comp) => {
|
|
330
|
-
switch (comp.type) {
|
|
331
|
-
case "buttons":
|
|
332
|
-
return this.getPaginationRow().toJSON();
|
|
333
|
-
|
|
334
|
-
case "data": {
|
|
335
|
-
const content = this.list
|
|
336
|
-
.slice(
|
|
337
|
-
page * this.entriesPerPage,
|
|
338
|
-
(page + 1) * this.entriesPerPage,
|
|
339
|
-
)
|
|
340
|
-
.join("\n");
|
|
341
|
-
return new TextDisplayBuilder()
|
|
342
|
-
.setContent(this.applyReplacements(content))
|
|
343
|
-
.toJSON();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// !! Default clause applies to the below as well
|
|
347
|
-
// case "display":
|
|
348
|
-
// case "section":
|
|
349
|
-
// case "separator":
|
|
350
|
-
// case "file":
|
|
351
|
-
// case "gallery":
|
|
352
|
-
// case "actionrow":
|
|
353
|
-
default:
|
|
354
|
-
return comp.component.toJSON();
|
|
355
|
-
}
|
|
356
|
-
}),
|
|
357
|
-
accent_color: this.styling?.[page]?.accent_color,
|
|
358
|
-
spoiler: this.styling?.[page]?.spoiler,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
private getPaginationRow(): ActionRowBuilder<ButtonBuilder> {
|
|
363
|
-
return new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
364
|
-
new ButtonBuilder()
|
|
365
|
-
.setCustomId(`${this.prefix}back`)
|
|
366
|
-
.setLabel("Prev")
|
|
367
|
-
.setStyle(ButtonStyle.Secondary)
|
|
368
|
-
.setDisabled(this.ended || this.currentIndex === 0),
|
|
369
|
-
new ButtonBuilder()
|
|
370
|
-
.setCustomId(`${this.prefix}info`)
|
|
371
|
-
.setLabel(
|
|
372
|
-
`${Math.floor(this.currentIndex / this.entriesPerPage) + 1}/${this.totalPages}`,
|
|
373
|
-
)
|
|
374
|
-
.setStyle(ButtonStyle.Secondary)
|
|
375
|
-
.setDisabled(this.ended || this.totalPages === 1),
|
|
376
|
-
new ButtonBuilder()
|
|
377
|
-
.setCustomId(`${this.prefix}forward`)
|
|
378
|
-
.setLabel("Next")
|
|
379
|
-
.setStyle(ButtonStyle.Secondary)
|
|
380
|
-
.setDisabled(
|
|
381
|
-
this.ended ||
|
|
382
|
-
this.currentIndex + this.entriesPerPage >= this.list.length,
|
|
383
|
-
),
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
499
|
private applyReplacements(content: string): string {
|
|
388
500
|
if (!this.replacements) return content;
|
|
389
501
|
return Object.entries(this.replacements).reduce(
|
|
@@ -394,15 +506,13 @@ export class DiscordPagination {
|
|
|
394
506
|
|
|
395
507
|
private async render(): Promise<void> {
|
|
396
508
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
allowedMentions: { parse: [], repliedUser: false },
|
|
405
|
-
});
|
|
509
|
+
const payload = this.buildPayload();
|
|
510
|
+
|
|
511
|
+
if (this.isMessage && this.replyMessage) {
|
|
512
|
+
await this.replyMessage.edit(payload);
|
|
513
|
+
} else if (this.interaction) {
|
|
514
|
+
await this.interaction.editReply(payload);
|
|
515
|
+
}
|
|
406
516
|
} catch (error) {
|
|
407
517
|
const e = error as Error;
|
|
408
518
|
if (!e.message.includes("Unknown Message")) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./LoggerModule";
|