-
Notifications
You must be signed in to change notification settings - Fork 147
feat(examples): add new examples about CV2 #1419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,9 +2,14 @@ | |||||||
|
|
||||||||
| """An example showcasing v2/layout components.""" | ||||||||
|
|
||||||||
| from __future__ import annotations | ||||||||
|
|
||||||||
| import datetime | ||||||||
| import os | ||||||||
| from typing import Any | ||||||||
| from pathlib import Path | ||||||||
| from typing import Any, List, NamedTuple | ||||||||
|
|
||||||||
| import disnake | ||||||||
| from disnake import ui | ||||||||
| from disnake.ext import commands | ||||||||
|
|
||||||||
|
|
@@ -29,6 +34,351 @@ async def send_components(ctx: commands.Context): | |||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| # Now let's make a simple command with static components | ||||||||
| # just to show some info | ||||||||
| class Website(NamedTuple): | ||||||||
| name: str | ||||||||
| domain: str | ||||||||
| web_url: str | ||||||||
| image_url: str | ||||||||
|
|
||||||||
|
|
||||||||
| async def fetch_websites() -> List[Website]: | ||||||||
| return [ | ||||||||
| Website( | ||||||||
| name="Disnake Dev", | ||||||||
| domain="disnake.dev", | ||||||||
| web_url="https://disnake.dev/", | ||||||||
| image_url="https://disnake.dev/assets/disnake-logo.png", | ||||||||
| ), | ||||||||
| Website( | ||||||||
| name="Disnake Docs", | ||||||||
| domain="docs.disnake.dev", | ||||||||
| web_url="https://docs.disnake.dev/en/stable/index.html", | ||||||||
| image_url="https://disnake.dev/assets/disnake-logo.png", | ||||||||
| ), | ||||||||
| Website( | ||||||||
| name="Disnake Guide", | ||||||||
| domain="guide.disnake.dev", | ||||||||
| web_url="https://guide.disnake.dev/", | ||||||||
| image_url="https://disnake.dev/assets/disnake-logo.png", | ||||||||
| ), | ||||||||
| ] | ||||||||
|
|
||||||||
|
|
||||||||
| @bot.slash_command() | ||||||||
| async def cool_message(inter: disnake.ApplicationCommandInteraction) -> None: | ||||||||
| # mock a fetch of the websites | ||||||||
| websites: list[Website] = await fetch_websites() | ||||||||
| web_components = [ | ||||||||
| ui.Section( | ||||||||
| ui.TextDisplay("### " + website.name), | ||||||||
| ui.TextDisplay(f"[`{website.domain}`]({website.web_url})"), | ||||||||
| accessory=ui.Thumbnail( | ||||||||
| media=website.image_url, description=f"{website.name}'s thumbnail" | ||||||||
| ), | ||||||||
| ) | ||||||||
| for website in websites | ||||||||
| ] | ||||||||
|
|
||||||||
| await inter.response.send_message( | ||||||||
| components=[ | ||||||||
| ui.Container( | ||||||||
| ui.TextDisplay(f"# Websites found ({len(websites)})"), | ||||||||
| *web_components, | ||||||||
| accent_colour=disnake.Color.blue(), | ||||||||
| ) | ||||||||
| ] | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| # Let's make an example about media gallery and sending local files | ||||||||
| @bot.slash_command() | ||||||||
| async def media_gallery(inter: disnake.ApplicationCommandInteraction) -> None: | ||||||||
| file_names = list(Path("assets/").glob("*.png")) | ||||||||
| media = [ | ||||||||
| disnake.MediaGalleryItem(media=f"attachment://{file_path.name}", description=file_path.name) | ||||||||
| for file_path in file_names | ||||||||
| ] | ||||||||
| files = [disnake.File(file_path, filename=file_path.name) for file_path in file_names] | ||||||||
| await inter.response.send_message( | ||||||||
| components=[ | ||||||||
| ui.Container( | ||||||||
| ui.TextDisplay("## Image Gallery"), | ||||||||
| ui.TextDisplay("A list of images present locally in the `assets` folder."), | ||||||||
| ui.MediaGallery(*media), | ||||||||
| ) | ||||||||
| ], | ||||||||
| files=files, | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| # mimic a DB call from a database | ||||||||
| async def fetch_user_todo_list(user_id: int) -> list[dict[str, Any]]: | ||||||||
| return [ | ||||||||
| { | ||||||||
| "id": 1, | ||||||||
| "title": "Study for the exam", | ||||||||
| "description": ":'(", | ||||||||
| "status": "Not Done", | ||||||||
| "deadline": datetime.datetime(year=2025, month=12, day=2, tzinfo=datetime.timezone.utc), | ||||||||
| }, | ||||||||
| { | ||||||||
| "id": 2, | ||||||||
| "title": "Finish other PRs", | ||||||||
| "description": "", | ||||||||
| "status": "Not Done", | ||||||||
| "deadline": datetime.datetime(year=2025, month=12, day=2, tzinfo=datetime.timezone.utc), | ||||||||
| }, | ||||||||
| { | ||||||||
| "id": 3, | ||||||||
| "title": "Make homemade pizza", | ||||||||
| "description": "", | ||||||||
| "status": "Not Done", | ||||||||
| "deadline": datetime.datetime(year=2025, month=12, day=2, tzinfo=datetime.timezone.utc), | ||||||||
| }, | ||||||||
| { | ||||||||
| "id": 4, | ||||||||
| "title": "Read the new book", | ||||||||
| "description": "", | ||||||||
| "status": "Not Done", | ||||||||
| "deadline": datetime.datetime(year=2025, month=12, day=2, tzinfo=datetime.timezone.utc), | ||||||||
| }, | ||||||||
| { | ||||||||
| "id": 5, | ||||||||
| "title": "Look for Christmas gift", | ||||||||
| "description": "", | ||||||||
| "status": "Not Done", | ||||||||
| "deadline": datetime.datetime(year=2025, month=12, day=2, tzinfo=datetime.timezone.utc), | ||||||||
| }, | ||||||||
| { | ||||||||
| "id": 6, | ||||||||
| "title": "Study for final exams", | ||||||||
| "description": "Pain", | ||||||||
| "status": "Not Done", | ||||||||
| "deadline": datetime.datetime(year=2026, month=1, day=2, tzinfo=datetime.timezone.utc), | ||||||||
| }, | ||||||||
| ] | ||||||||
|
|
||||||||
|
|
||||||||
| # Now let's make a more complex example that uses buttons | ||||||||
| @bot.slash_command() | ||||||||
| async def todo_list(inter: disnake.ApplicationCommandInteraction) -> None: | ||||||||
| TODO_PER_PAGE = 5 | ||||||||
| data = await fetch_user_todo_list(inter.author.id) | ||||||||
|
|
||||||||
| last_page_size = len(data) % TODO_PER_PAGE | ||||||||
| total_pages = len(data) // TODO_PER_PAGE | ||||||||
|
Comment on lines
+170
to
+171
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| if last_page_size != 0: | ||||||||
| total_pages += 1 | ||||||||
|
|
||||||||
| paginator_buttons_disabled = len(data) == TODO_PER_PAGE | ||||||||
| await send_page(inter, 0, total_pages, last_page_size, paginator_buttons_disabled) | ||||||||
|
|
||||||||
|
|
||||||||
| async def interaction_check( | ||||||||
| inter: disnake.MessageInteraction, invoker_id: int, user_id: int | ||||||||
| ) -> None: | ||||||||
| if int(invoker_id) != user_id: | ||||||||
| await inter.send("You can't interact with this component :(", ephemeral=True) | ||||||||
|
|
||||||||
|
|
||||||||
| async def send_page( | ||||||||
| inter: disnake.ApplicationCommandInteraction | disnake.MessageInteraction, | ||||||||
| current_page: int, | ||||||||
| total_pages: int, | ||||||||
| last_page_size: int, | ||||||||
| paginator_buttons_disabled: bool = False, | ||||||||
| ) -> None: | ||||||||
| # ideally you cache this somehow instead of making one DB call for every button click | ||||||||
| data = await fetch_user_todo_list(inter.author.id) | ||||||||
| pages = [] | ||||||||
| base = 0 | ||||||||
|
|
||||||||
| if data: | ||||||||
| # we split our data nicely into pages | ||||||||
| # ideally you don't do this for every button click, you just do it the first time | ||||||||
| # then cache it and reuse | ||||||||
|
Comment on lines
+200
to
+201
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is that? |
||||||||
| for _ in range(total_pages): | ||||||||
| page = [] | ||||||||
| for j in range(base, base + 5): | ||||||||
| if j >= len(data): | ||||||||
| break | ||||||||
|
|
||||||||
| d = data[j] | ||||||||
| page.append( | ||||||||
| ui.Section( | ||||||||
| ui.TextDisplay(f"## {d['title']}"), | ||||||||
| ui.TextDisplay( | ||||||||
| f"{d['description']}\n**{d['status']}** • {disnake.utils.format_dt(d['deadline'])}" | ||||||||
| ), | ||||||||
| # just some empty chars, don't mind them | ||||||||
| accessory=ui.Button( | ||||||||
| label="⋮ Options", # noqa: PLE2502 | ||||||||
| custom_id=f"todo_options:{inter.author.id}:{d['id']}", | ||||||||
| ), | ||||||||
| ) | ||||||||
| ) | ||||||||
|
|
||||||||
| # we don't put separators after the last element of | ||||||||
| # a page and, in the last page, after the last element (of a non fully filled page) | ||||||||
| if ((j + 1) % 5 != 0) and j != len(data) - 1: | ||||||||
| page.append(ui.Separator(spacing=disnake.SeparatorSpacing.large)) | ||||||||
| base += 5 | ||||||||
| pages.append(page) | ||||||||
| else: | ||||||||
| pages.append(ui.TextDisplay("__No TODOs yet :(__")) | ||||||||
|
|
||||||||
| components = [ | ||||||||
| ui.Container( | ||||||||
| ui.TextDisplay(f"# `{inter.author}`'s TODO list"), | ||||||||
| *pages[current_page], | ||||||||
| ), | ||||||||
| ui.ActionRow( | ||||||||
| ui.Button( | ||||||||
| emoji="⏪", | ||||||||
| custom_id=f"todo_f_back_btn:{inter.author.id}:{current_page}:{total_pages}:{last_page_size}", | ||||||||
| # this button is disabled if all the buttons for the paginator are disabled or | ||||||||
| # if we are at the first page | ||||||||
| disabled=paginator_buttons_disabled or (current_page == 0), | ||||||||
| ), | ||||||||
| ui.Button( | ||||||||
| emoji="◀️", | ||||||||
| custom_id=f"todo_back_btn:{inter.author.id}:{current_page}:{total_pages}:{last_page_size}", | ||||||||
| disabled=paginator_buttons_disabled, | ||||||||
| ), | ||||||||
| ui.Button(label=f"{current_page + 1}/{total_pages}", disabled=True), | ||||||||
| ui.Button( | ||||||||
| emoji="▶️", | ||||||||
| custom_id=f"todo_next_btn:{inter.author.id}:{current_page}:{total_pages}:{last_page_size}", | ||||||||
| disabled=paginator_buttons_disabled, | ||||||||
| ), | ||||||||
| ui.Button( | ||||||||
| emoji="⏩", | ||||||||
| custom_id=f"todo_f_next_btn:{inter.author.id}:{current_page}:{total_pages}:{last_page_size}", | ||||||||
| # this button is disabled if all the buttons for the paginator are disabled or | ||||||||
| # if we are at the last page | ||||||||
| disabled=paginator_buttons_disabled or (current_page == (total_pages - 1)), | ||||||||
| ), | ||||||||
| ), | ||||||||
| ] | ||||||||
|
|
||||||||
| if isinstance(inter, disnake.ApplicationCommandInteraction): | ||||||||
| # this means that we are sending the first page | ||||||||
| return await inter.send(components=components) | ||||||||
| await inter.response.edit_message(components=components) | ||||||||
|
|
||||||||
|
|
||||||||
| @bot.listen(disnake.Event.button_click) | ||||||||
| async def back_btn(inter: disnake.MessageInteraction) -> None: | ||||||||
| if not inter.component.custom_id: | ||||||||
| return | ||||||||
|
|
||||||||
| # remember that this is a normal listener and will get called | ||||||||
| # for every global button click so we ignore every other component | ||||||||
| # except for the one we really care (todo_back_btn) | ||||||||
| if not inter.component.custom_id.startswith("todo_back_btn"): | ||||||||
| return | ||||||||
|
|
||||||||
| # we get our data from the button custom id that we built previously | ||||||||
| invoker_id, current_page, total_pages, last_page_size = map( | ||||||||
| int, inter.component.custom_id.split(":")[1:] | ||||||||
| ) | ||||||||
| await interaction_check(inter, invoker_id, inter.author.id) | ||||||||
|
|
||||||||
| # we implement a pac-man like effect, if you are at the first page | ||||||||
| # it will bring you at the last page | ||||||||
| if current_page == 0: | ||||||||
| current_page = total_pages - 1 | ||||||||
| else: | ||||||||
| current_page -= 1 | ||||||||
|
|
||||||||
| await send_page(inter, current_page, total_pages, last_page_size) | ||||||||
|
|
||||||||
|
|
||||||||
| @bot.listen(disnake.Event.button_click) | ||||||||
| async def fast_back_btn(inter: disnake.MessageInteraction) -> None: | ||||||||
| if not inter.component.custom_id: | ||||||||
| return | ||||||||
|
|
||||||||
| if not inter.component.custom_id.startswith("todo_f_back_btn"): | ||||||||
| return | ||||||||
|
|
||||||||
| # remember that this is a normal listener and will get called | ||||||||
| # for every global button click so we ignore every other component | ||||||||
| # except for the one we really care (todo_next_btn) | ||||||||
| invoker_id, current_page, total_pages, last_page_size = map( | ||||||||
| int, inter.component.custom_id.split(":")[1:] | ||||||||
| ) | ||||||||
| await interaction_check(inter, invoker_id, inter.author.id) | ||||||||
|
|
||||||||
| # bring the user to the first page | ||||||||
| current_page = 0 | ||||||||
| await send_page(inter, current_page, total_pages, last_page_size) | ||||||||
|
|
||||||||
|
|
||||||||
| @bot.listen(disnake.Event.button_click) | ||||||||
| async def next_btn(inter: disnake.MessageInteraction) -> None: | ||||||||
| if not inter.component.custom_id: | ||||||||
| return | ||||||||
|
|
||||||||
| if not inter.component.custom_id.startswith("todo_next_btn"): | ||||||||
| return | ||||||||
|
|
||||||||
| # remember that this is a normal listener and will get called | ||||||||
| # for every global button click so we ignore every other component | ||||||||
| # except for the one we really care (todo_next_btn) | ||||||||
| invoker_id, current_page, total_pages, last_page_size = map( | ||||||||
| int, inter.component.custom_id.split(":")[1:] | ||||||||
| ) | ||||||||
| await interaction_check(inter, invoker_id, inter.author.id) | ||||||||
|
|
||||||||
| # we implement a pac-man like effect, if you are at the last page | ||||||||
| # it will bring you at the first page | ||||||||
| if current_page == (total_pages - 1): | ||||||||
| current_page = 0 | ||||||||
| else: | ||||||||
| current_page += 1 | ||||||||
|
|
||||||||
| await send_page(inter, current_page, total_pages, last_page_size) | ||||||||
|
|
||||||||
|
|
||||||||
| @bot.listen(disnake.Event.button_click) | ||||||||
| async def fast_next_btn(inter: disnake.MessageInteraction) -> None: | ||||||||
| if not inter.component.custom_id: | ||||||||
| return | ||||||||
|
|
||||||||
| if not inter.component.custom_id.startswith("todo_f_next_btn"): | ||||||||
| return | ||||||||
|
|
||||||||
| # remember that this is a normal listener and will get called | ||||||||
| # for every global button click so we ignore every other component | ||||||||
| # except for the one we really care (todo_next_btn) | ||||||||
| invoker_id, current_page, total_pages, last_page_size = map( | ||||||||
| int, inter.component.custom_id.split(":")[1:] | ||||||||
| ) | ||||||||
| await interaction_check(inter, invoker_id, inter.author.id) | ||||||||
|
|
||||||||
| # bring the user to the last page | ||||||||
| current_page = total_pages - 1 | ||||||||
| await send_page(inter, current_page, total_pages, last_page_size) | ||||||||
|
|
||||||||
|
|
||||||||
| @bot.listen(disnake.Event.button_click) | ||||||||
| async def options_btn(inter: disnake.MessageInteraction) -> None: | ||||||||
| if not inter.component.custom_id: | ||||||||
| return | ||||||||
|
|
||||||||
| if not inter.component.custom_id.startswith("todo_options"): | ||||||||
| return | ||||||||
|
|
||||||||
| invoker_id, _ = map(int, inter.component.custom_id.split(":")[1:]) | ||||||||
| await interaction_check(inter, invoker_id, inter.author.id) | ||||||||
| await inter.send( | ||||||||
| "Implement this logic yourself! You should now understand how this works.", ephemeral=True | ||||||||
| ) | ||||||||
|
Comment on lines
+272
to
+379
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a point in having a separate listener for each subcomponent |
||||||||
|
|
||||||||
|
|
||||||||
| @bot.event | ||||||||
| async def on_ready(): | ||||||||
| print(f"Logged in as {bot.user} (ID: {bot.user.id})\n------") | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.