|
| 1 | +# Copyright (c) NiceBots all rights reserved |
| 2 | +from collections import defaultdict |
| 3 | +from functools import cached_property |
| 4 | +from typing import Any, Final, final, override |
| 5 | + |
| 6 | +import discord |
| 7 | +from discord.ext import commands |
| 8 | +from discord.ext import pages as paginator |
| 9 | + |
| 10 | +from src import custom |
| 11 | +from src.extensions.help.pages.classes import ( |
| 12 | + HelpCategoryTranslation, |
| 13 | +) |
| 14 | +from src.i18n.classes import RawTranslation, TranslationWrapper, apply_locale |
| 15 | + |
| 16 | +from .pages import help_translation |
| 17 | + |
| 18 | + |
| 19 | +def get_gradient_color(shade_index: int, color_index: int, max_shade: int = 50, max_color: int = 10) -> int: |
| 20 | + """Generate a color from a two-dimensional gradient system using bright pastel colors. |
| 21 | +
|
| 22 | + Args: |
| 23 | + ---- |
| 24 | + shade_index (int): Index for shade selection (0 to max_shade) |
| 25 | + color_index (int): Index for base color selection (0 to max_color) |
| 26 | + max_shade (int): Maximum value for shade_index (default 50) |
| 27 | + max_color (int): Maximum value for color_index (default 10) |
| 28 | +
|
| 29 | + Returns: |
| 30 | + ------- |
| 31 | + int: Color as a 24-bit integer |
| 32 | +
|
| 33 | + """ |
| 34 | + # Normalize indices to 0-1 range |
| 35 | + shade_factor = max(0, min(1, shade_index / max_shade)) |
| 36 | + color_factor = max(0, min(1, color_index / max_color)) |
| 37 | + |
| 38 | + # Bright pastel base colors |
| 39 | + base_colors = [ |
| 40 | + (179, 229, 252), # Bright light blue |
| 41 | + (225, 190, 231), # Bright lilac |
| 42 | + (255, 209, 220), # Bright pink |
| 43 | + (255, 224, 178), # Bright peach |
| 44 | + (255, 255, 198), # Bright yellow |
| 45 | + (200, 230, 201), # Bright mint |
| 46 | + (178, 255, 255), # Bright turquoise |
| 47 | + (187, 222, 251), # Bright baby blue |
| 48 | + (225, 190, 231), # Bright lavender |
| 49 | + (255, 236, 179), # Bright cream |
| 50 | + (200, 230, 255), # Bright sky blue |
| 51 | + ] |
| 52 | + |
| 53 | + # Interpolate between colors based on color_factor |
| 54 | + color_index_float = color_factor * (len(base_colors) - 1) |
| 55 | + color_index_low = int(color_index_float) |
| 56 | + color_index_high = min(color_index_low + 1, len(base_colors) - 1) |
| 57 | + color_blend = color_index_float - color_index_low |
| 58 | + |
| 59 | + c1 = base_colors[color_index_low] |
| 60 | + c2 = base_colors[color_index_high] |
| 61 | + |
| 62 | + # Interpolate between the two closest base colors |
| 63 | + base_color = tuple(int(c1[i] * (1 - color_blend) + c2[i] * color_blend) for i in range(3)) |
| 64 | + |
| 65 | + # Modified shading approach for brighter colors |
| 66 | + if shade_factor < 0.5: |
| 67 | + # Darker shades: interpolate towards a very light gray instead of black |
| 68 | + shade_factor_adjusted = shade_factor * 2 |
| 69 | + # Increase minimum brightness (was 120, now 180) |
| 70 | + darker_tone = tuple(max(c * 0.8, 180) for c in base_color) |
| 71 | + final_color = tuple( |
| 72 | + int(darker_tone[i] + (base_color[i] - darker_tone[i]) * shade_factor_adjusted) for i in range(3) |
| 73 | + ) |
| 74 | + else: |
| 75 | + # Lighter shades: interpolate towards white |
| 76 | + shade_factor_adjusted = (shade_factor - 0.5) * 2 |
| 77 | + final_color = tuple( |
| 78 | + int(base_color[i] * (1 - shade_factor_adjusted) + 255 * shade_factor_adjusted) for i in range(3) |
| 79 | + ) |
| 80 | + |
| 81 | + # Convert to 24-bit integer |
| 82 | + return (final_color[0] << 16) | (final_color[1] << 8) | final_color[2] |
| 83 | + |
| 84 | + |
| 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 | + |
| 90 | +@final |
| 91 | +class HelpView(paginator.Paginator): |
| 92 | + def __init__( |
| 93 | + self, |
| 94 | + embeds: dict[str, list[discord.Embed]], |
| 95 | + ui_translations: TranslationWrapper[dict[str, RawTranslation]], |
| 96 | + bot: custom.Bot, |
| 97 | + ) -> None: |
| 98 | + self.bot = bot |
| 99 | + |
| 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 | + ] |
| 108 | + |
| 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, |
| 124 | + ) |
| 125 | + |
| 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] |
| 134 | + |
| 135 | + |
| 136 | +def get_categories_embeds( |
| 137 | + ui_translations: TranslationWrapper[dict[str, RawTranslation]], |
| 138 | + categories: dict[str, TranslationWrapper[HelpCategoryTranslation]], |
| 139 | + bot: custom.Bot, |
| 140 | +) -> dict[str, list[discord.Embed]]: |
| 141 | + embeds: defaultdict[str, list[discord.Embed]] = defaultdict(list) |
| 142 | + for i, category in enumerate(categories): |
| 143 | + 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 | + ) |
| 149 | + if page.quick_tips: |
| 150 | + embed.add_field(name=ui_translations.quick_tips_title, value="- " + "\n- ".join(page.quick_tips)) # pyright: ignore [reportUnknownArgumentType] |
| 151 | + if page.examples: |
| 152 | + embed.add_field(name=ui_translations.examples_title, value="- " + "\n- ".join(page.examples)) # pyright: ignore [reportUnknownArgumentType] |
| 153 | + 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) |
| 161 | + |
| 162 | + |
| 163 | +@final |
| 164 | +class Help(commands.Cog): |
| 165 | + def __init__(self, bot: custom.Bot, ui_translations: dict[str, RawTranslation], locales: set[str]) -> None: |
| 166 | + self.bot = bot |
| 167 | + self.ui_translations = ui_translations |
| 168 | + self.locales = locales |
| 169 | + |
| 170 | + @cached_property |
| 171 | + async def embeds(self) -> dict[str, dict[str, list[discord.Embed]]]: |
| 172 | + embeds: defaultdict[str, dict[str, list[discord.Embed]]] = defaultdict(dict) |
| 173 | + for locale in self.locales: |
| 174 | + t = help_translation.get_for_locale(locale) |
| 175 | + ui = apply_locale(self.ui_translations, locale) |
| 176 | + embeds[locale] = get_categories_embeds(ui, t.categories, self.bot) |
| 177 | + return dict(embeds) |
| 178 | + |
| 179 | + @discord.slash_command( |
| 180 | + name="help", |
| 181 | + integration_types={discord.IntegrationType.user_install, discord.IntegrationType.guild_install}, |
| 182 | + contexts={ |
| 183 | + discord.InteractionContextType.guild, |
| 184 | + discord.InteractionContextType.private_channel, |
| 185 | + discord.InteractionContextType.bot_dm, |
| 186 | + }, |
| 187 | + ) |
| 188 | + async def help_slash(self, ctx: custom.ApplicationContext) -> None: |
| 189 | + paginator = HelpView( |
| 190 | + embeds=self.embeds[ctx.locale], |
| 191 | + ui_translations=apply_locale(self.ui_translations, ctx.locale), |
| 192 | + bot=self.bot, |
| 193 | + ) |
| 194 | + await paginator.respond(ctx.interaction, ephemeral=True) |
| 195 | + |
| 196 | + |
| 197 | +def setup(bot: custom.Bot, config: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny] |
| 198 | + bot.add_cog(Help(bot, config["translations"], set(config["locales"]))) |
| 199 | + |
| 200 | + |
| 201 | +default: Final = {"enabled": False} |
| 202 | +__all__ = ["default", "setup"] |
0 commit comments