11# Copyright (c) NiceBots all rights reserved
22from collections import defaultdict
33from functools import cached_property
4- from typing import Any , Final , final , override
4+ from typing import Any , Final , final
55
66import discord
77from discord .ext import commands
8- from discord .ext import pages as paginator
8+ from discord .ui import (
9+ Button ,
10+ Container ,
11+ Select ,
12+ TextDisplay ,
13+ View ,
14+ )
915
1016from src import custom
1117from src .extensions .help .pages .classes import (
@@ -82,82 +88,205 @@ def get_gradient_color(shade_index: int, color_index: int, max_shade: int = 50,
8288 return (final_color [0 ] << 16 ) | (final_color [1 ] << 8 ) | final_color [2 ]
8389
8490
85- class PageIndicatorButton (paginator .PaginatorButton ):
86- def __init__ (self ) -> None :
87- super ().__init__ (button_type = "page_indicator" , disabled = True , label = "" , style = discord .ButtonStyle .gray )
88-
89-
9091@final
91- class HelpView (paginator . Paginator ):
92+ class HelpView (View ):
9293 def __init__ (
9394 self ,
94- embeds : dict [str , list [discord . Embed ]],
95+ categories_data : dict [str , list [dict ]],
9596 ui_translations : TranslationWrapper [dict [str , RawTranslation ]],
9697 bot : custom .Bot ,
9798 ) -> None :
99+ super ().__init__ (timeout = 180 )
98100 self .bot = bot
101+ self .categories_data = categories_data
102+ self .ui_translations = ui_translations
103+ self .current_category = next (list (categories_data .keys ()))
104+ self .current_page = 0
105+ self .total_pages = len (categories_data [self .current_category ])
99106
100- the_pages : list [paginator .PageGroup ] = [
101- paginator .PageGroup (
102- [paginator .Page (embeds = [embed ]) for embed in data [1 ]],
103- label = data [0 ],
104- default = i == 0 ,
105- )
106- for i , data in enumerate (embeds .items ())
107- ]
107+ # Create navigation buttons (will be added to container in update_content)
108+ self .first_button = Button (style = discord .ButtonStyle .blurple , emoji = "⏮️" , label = "First" , disabled = True )
109+ self .first_button .callback = self .first_button_callback
108110
109- self .embeds = embeds
110- self .ui_translations = ui_translations
111- self .page_indicator = PageIndicatorButton ()
112- super ().__init__ (
113- the_pages ,
114- show_menu = True ,
115- menu_placeholder = ui_translations .select_category ,
116- custom_buttons = [
117- paginator .PaginatorButton ("first" , emoji = "⏮️" , style = discord .ButtonStyle .blurple ),
118- paginator .PaginatorButton ("prev" , emoji = "◀️" , style = discord .ButtonStyle .red ),
119- self .page_indicator ,
120- paginator .PaginatorButton ("next" , emoji = "▶️" , style = discord .ButtonStyle .green ),
121- paginator .PaginatorButton ("last" , emoji = "⏭️" , style = discord .ButtonStyle .blurple ),
122- ],
123- use_default_buttons = False ,
111+ self .prev_button = Button (style = discord .ButtonStyle .red , emoji = "◀️" , label = "Previous" , disabled = True )
112+ self .prev_button .callback = self .prev_button_callback
113+
114+ self .page_indicator = Button (
115+ style = discord .ButtonStyle .gray ,
116+ label = ui_translations .page_indicator .format (current = 1 , total = self .total_pages ),
117+ disabled = True ,
118+ )
119+
120+ self .next_button = Button (
121+ style = discord .ButtonStyle .green , emoji = "▶️" , label = "Next" , disabled = self .total_pages <= 1
122+ )
123+ self .next_button .callback = self .next_button_callback
124+
125+ self .last_button = Button (
126+ style = discord .ButtonStyle .blurple , emoji = "⏭️" , label = "Last" , disabled = self .total_pages <= 1
127+ )
128+ self .last_button .callback = self .last_button_callback
129+
130+ # Create category selector
131+ self .category_select = Select (
132+ placeholder = ui_translations .select_category ,
133+ options = [discord .SelectOption (label = category , value = category ) for category in categories_data ],
134+ )
135+ self .category_select .callback = self .category_select_callback
136+
137+ # Add the main container with content
138+ self .update_content ()
139+
140+ def update_content (self ) -> None :
141+ """Update the view's content based on the current category and page."""
142+ # Remove existing containers if any
143+ for item in list (self .children ):
144+ if isinstance (item , Container ):
145+ self .remove_item (item )
146+
147+ # Get the current page data
148+ page_data = self .categories_data [self .current_category ][self .current_page ]
149+
150+ # Create the main container
151+ container = Container (color = discord .Color (page_data ["color" ]))
152+
153+ # Add title directly as TextDisplay
154+ title_text = TextDisplay (f"### { page_data ['title' ]} " )
155+ container .add_item (title_text )
156+
157+ # Add description directly as TextDisplay
158+ description_text = TextDisplay (page_data ["description" ])
159+ container .add_item (description_text )
160+
161+ # Add separator
162+ container .add_separator (divider = True )
163+
164+ # Add quick tips if available
165+ if page_data .get ("quick_tips" ):
166+ tips_title = TextDisplay (f"### { self .ui_translations .quick_tips_title } " )
167+ container .add_item (tips_title )
168+ tips_content = TextDisplay ("\n " .join ([f"- { tip } " for tip in page_data ["quick_tips" ]]))
169+ container .add_item (tips_content )
170+ container .add_separator ()
171+
172+ # Add examples if available
173+ if page_data .get ("examples" ):
174+ examples_title = TextDisplay (f"### { self .ui_translations .examples_title } " )
175+ container .add_item (examples_title )
176+ examples_content = TextDisplay ("\n " .join ([f"- { example } " for example in page_data ["examples" ]]))
177+ container .add_item (examples_content )
178+ container .add_separator ()
179+
180+ # Add related commands if available
181+ if page_data .get ("related_commands" ):
182+ commands_title = TextDisplay (f"### { self .ui_translations .related_commands_title } " )
183+ commands_content_list = []
184+ for cmd_name in page_data ["related_commands" ]:
185+ cmd = self .bot .get_application_command (cmd_name )
186+ if cmd :
187+ commands_content_list .append (f"- { cmd .mention } " )
188+ if commands_content_list :
189+ container .add_item (commands_title )
190+ commands_content = TextDisplay ("\n " .join (commands_content_list ))
191+ container .add_item (commands_content )
192+ container .add_separator (divider = True )
193+
194+ # Add navigation buttons to the container
195+ container .add_item (self .first_button )
196+ container .add_item (self .prev_button )
197+ container .add_item (self .page_indicator )
198+ container .add_item (self .next_button )
199+ container .add_item (self .last_button )
200+ container .add_item (self .category_select )
201+
202+ # Add the container to the view
203+ self .add_item (container )
204+
205+ async def category_select_callback (self , interaction : discord .Interaction ) -> None :
206+ """Handle category selection."""
207+ self .current_category = self .category_select .values [0 ]
208+ self .current_page = 0
209+ self .total_pages = len (self .categories_data [self .current_category ])
210+
211+ # Update navigation buttons
212+ self .first_button .disabled = True
213+ self .prev_button .disabled = True
214+ self .next_button .disabled = self .total_pages <= 1
215+ self .last_button .disabled = self .total_pages <= 1
216+ self .page_indicator .label = self .ui_translations .page_indicator .format (current = 1 , total = self .total_pages )
217+
218+ # Update content
219+ self .update_content ()
220+ await interaction .response .edit_message (view = self )
221+
222+ async def first_button_callback (self , interaction : discord .Interaction ) -> None :
223+ """Go to the first page."""
224+ self .current_page = 0
225+ await self .update_page (interaction )
226+
227+ async def prev_button_callback (self , interaction : discord .Interaction ) -> None :
228+ """Go to the previous page."""
229+ self .current_page = max (0 , self .current_page - 1 )
230+ await self .update_page (interaction )
231+
232+ async def next_button_callback (self , interaction : discord .Interaction ) -> None :
233+ """Go to the next page."""
234+ self .current_page = min (self .total_pages - 1 , self .current_page + 1 )
235+ await self .update_page (interaction )
236+
237+ async def last_button_callback (self , interaction : discord .Interaction ) -> None :
238+ """Go to the last page."""
239+ self .current_page = self .total_pages - 1
240+ await self .update_page (interaction )
241+
242+ async def update_page (self , interaction : discord .Interaction ) -> None :
243+ """Update the view after a page change."""
244+ # Update navigation buttons
245+ self .first_button .disabled = self .current_page == 0
246+ self .prev_button .disabled = self .current_page == 0
247+ self .next_button .disabled = self .current_page == self .total_pages - 1
248+ self .last_button .disabled = self .current_page == self .total_pages - 1
249+ self .page_indicator .label = self .ui_translations .page_indicator .format (
250+ current = self .current_page + 1 , total = self .total_pages
124251 )
125252
126- @override
127- def update_buttons (self ) -> dict : # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
128- r = super ().update_buttons () # pyright: ignore [reportUnknownVariableType]
129- if self .show_indicator :
130- self .buttons ["page_indicator" ]["object" ].label = self .ui_translations .page_indicator .format (
131- current = self .current_page + 1 , total = self .page_count + 1
132- )
133- return r # pyright: ignore [reportUnknownVariableType]
253+ # Update content
254+ self .update_content ()
255+ await interaction .response .edit_message (view = self )
134256
135257
136- def get_categories_embeds (
137- ui_translations : TranslationWrapper [dict [str , RawTranslation ]],
258+ def get_categories_data (
259+ ui_translations : TranslationWrapper [dict [str , RawTranslation ]], # noqa: ARG001
138260 categories : dict [str , TranslationWrapper [HelpCategoryTranslation ]],
139- bot : custom .Bot ,
140- ) -> dict [str , list [discord .Embed ]]:
141- embeds : defaultdict [str , list [discord .Embed ]] = defaultdict (list )
261+ bot : custom .Bot , # noqa: ARG001
262+ ) -> dict [str , list [dict ]]:
263+ """Generate category data for the help view.
264+
265+ Returns a dictionary where keys are category names and values are lists of page data dictionaries.
266+ Each page data dictionary contains title, description, color, and optional quick_tips,
267+ examples, and related_commands.
268+ """
269+ categories_data : defaultdict [str , list [dict ]] = defaultdict (list )
142270 for i , category in enumerate (categories ):
143271 for j , page in enumerate (category .pages .values ()): # pyright: ignore [reportUnknownArgumentType, reportUnknownVariableType, reportAttributeAccessIssue]
144- embed = discord .Embed (
145- title = f"{ category .name } - { page .title } " , # pyright: ignore [reportAttributeAccessIssue]
146- description = page .description , # pyright: ignore [reportUnknownArgumentType]
147- color = discord .Color (get_gradient_color (i , j )),
148- )
272+ page_data = {
273+ "title" : f"{ category .name } - { page .title } " , # pyright: ignore [reportAttributeAccessIssue]
274+ "description" : page .description , # pyright: ignore [reportUnknownArgumentType]
275+ "color" : get_gradient_color (i , j ),
276+ }
277+
149278 if page .quick_tips :
150- embed .add_field (name = ui_translations .quick_tips_title , value = "- " + "\n - " .join (page .quick_tips )) # pyright: ignore [reportUnknownArgumentType]
279+ page_data ["quick_tips" ] = page .quick_tips # pyright: ignore [reportUnknownArgumentType]
280+
151281 if page .examples :
152- embed .add_field (name = ui_translations .examples_title , value = "- " + "\n - " .join (page .examples )) # pyright: ignore [reportUnknownArgumentType]
282+ page_data ["examples" ] = page .examples # pyright: ignore [reportUnknownArgumentType]
283+
153284 if page .related_commands :
154- embed .add_field (
155- name = ui_translations .related_commands_title ,
156- value = "- "
157- + "\n - " .join (bot .get_application_command (name ).mention for name in page .related_commands ), # pyright: ignore [reportUnknownArgumentType, reportUnknownVariableType, reportAttributeAccessIssue, reportOptionalMemberAccess]
158- )
159- embeds [category .name ].append (embed ) # pyright: ignore [reportAttributeAccessIssue]
160- return dict (embeds )
285+ page_data ["related_commands" ] = page .related_commands # pyright: ignore [reportUnknownArgumentType, reportUnknownVariableType, reportAttributeAccessIssue]
286+
287+ categories_data [category .name ].append (page_data ) # pyright: ignore [reportAttributeAccessIssue]
288+
289+ return dict (categories_data )
161290
162291
163292@final
@@ -168,13 +297,14 @@ def __init__(self, bot: custom.Bot, ui_translations: dict[str, RawTranslation],
168297 self .locales = locales
169298
170299 @cached_property
171- def embeds (self ) -> dict [str , dict [str , list [discord .Embed ]]]:
172- embeds : defaultdict [str , dict [str , list [discord .Embed ]]] = defaultdict (dict )
300+ def categories_data (self ) -> dict [str , dict [str , list [dict ]]]:
301+ """Generate and cache help category data for all locales."""
302+ data : defaultdict [str , dict [str , list [dict ]]] = defaultdict (dict )
173303 for locale in self .locales :
174304 t = help_translation .get_for_locale (locale )
175305 ui = apply_locale (self .ui_translations , locale )
176- embeds [locale ] = get_categories_embeds (ui , t .categories , self .bot )
177- return dict (embeds )
306+ data [locale ] = get_categories_data (ui , t .categories , self .bot )
307+ return dict (data )
178308
179309 @discord .slash_command (
180310 name = "help" ,
@@ -186,12 +316,13 @@ def embeds(self) -> dict[str, dict[str, list[discord.Embed]]]:
186316 },
187317 )
188318 async def help_slash (self , ctx : custom .ApplicationContext ) -> None :
189- paginator = HelpView (
190- embeds = self .embeds [ctx .locale ],
319+ """Display help information using the new UI components."""
320+ help_view = HelpView (
321+ categories_data = self .categories_data [ctx .locale ],
191322 ui_translations = apply_locale (self .ui_translations , ctx .locale ),
192323 bot = self .bot ,
193324 )
194- await paginator .respond (ctx . interaction , ephemeral = True )
325+ await ctx .respond (view = help_view , ephemeral = True )
195326
196327
197328def setup (bot : custom .Bot , config : dict [str , Any ]) -> None : # pyright: ignore [reportExplicitAny]
0 commit comments