Skip to content

Commit 41666ff

Browse files
committed
♻️ Refactor help view to use new UI components and improve category data handling
1 parent dbd4b8f commit 41666ff

File tree

4 files changed

+210
-77
lines changed

4 files changed

+210
-77
lines changed

Dockerfile

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ ENV PDM_CHECK_UPDATE=false
1414
WORKDIR /app
1515
COPY src pyproject.toml pdm.lock ./
1616

17-
RUN pdm export --prod -o requirements.txt
17+
&& pip install -r requirements.txt
1818

1919
FROM python:${PYTHON_VERSION}-slim-bookworm AS app
2020

@@ -29,7 +29,9 @@ COPY --from=python-base --chown=appuser /app/requirements.txt ./
2929
COPY src/ ./src
3030
COPY LICENSE ./
3131

32-
RUN pip install -r requirements.txt --require-hashes
32+
RUN pip install $(grep '^pycord-rest-bot==' requirements.txt | tr -d '\\') \
33+
&& sed -i '/pycord-rest-bot/d' requirements.txt \
34+
&& pip install -r requirements.txt
3335
USER appuser
3436

3537
CMD ["python", "src"]

pdm.lock

+7-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors = [
66
{name = "Paillat-dev", email = "[email protected]"},
77
]
88
dependencies = [
9-
"py-cord>=2.5.0",
9+
"py-cord @ git+https://github.com/Pycord-Development/pycord.git@refs/pull/2707/head",
1010
"aiohttp>=3.9.5",
1111
"pyyaml>=6.0.1",
1212
"python-dotenv>=1.0.1",
@@ -23,6 +23,7 @@ dependencies = [
2323
"tortoise-orm[asyncpg]>=0.23.0",
2424
"aerich[toml]>=0.8.1",
2525
"pycord-rest-bot>=0.1.0a7",
26+
"git+https://github.com/Pycord-Development/pycord.git@refs/pull/2707/head",
2627
]
2728
requires-python = "==3.12.*"
2829
readme = "README.md"

src/extensions/help/__init__.py

+197-66
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# Copyright (c) NiceBots all rights reserved
22
from collections import defaultdict
33
from functools import cached_property
4-
from typing import Any, Final, final, override
4+
from typing import Any, Final, final
55

66
import discord
77
from 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

1016
from src import custom
1117
from 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

197328
def setup(bot: custom.Bot, config: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny]

0 commit comments

Comments
 (0)