feat: Refactor leveling commands to use UI components for enhanced user experience

This commit is contained in:
Slipstream 2025-05-30 19:23:13 -06:00
parent 8fbfb8eb65
commit 859b20dfe1
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD

View File

@ -1,5 +1,6 @@
import discord
from discord.ext import commands
from discord import ui # Add ui for LayoutView
import json
import os
import asyncio
@ -373,14 +374,30 @@ class LevelingCog(commands.Cog):
filled_length = int(progress_bar_length * progress)
bar = '' * filled_length + '' * (progress_bar_length - filled_length)
embed = discord.Embed(
title=f"{target.display_name}'s Level",
description=f"**Level:** {level}\n**XP:** {xp} / {xp_needed}\n\n**Progress to Level {next_level}:**\n[{bar}] {int(progress * 100)}%",
color=discord.Color.blue()
)
embed.set_thumbnail(url=target.display_avatar.url)
class LevelCheckView(ui.LayoutView):
def __init__(self, target_member: discord.Member, level: int, xp: int, xp_needed: int, next_level: int, bar: str, progress_percent: int):
super().__init__()
await ctx.send(embed=embed)
# Container for overall layout, possibly with accent color
container = ui.Container(accent_colour=discord.Color.blue())
self.add_item(container)
# Title
container.add_item(ui.TextDisplay(f"**{target_member.display_name}'s Level**")) # Using TextDisplay for title
# Thumbnail
if target_member.display_avatar:
container.add_item(ui.Thumbnail(media=target_member.display_avatar.url, description="User Avatar"))
# Main content
container.add_item(ui.TextDisplay(f"**Level:** {level}"))
container.add_item(ui.TextDisplay(f"**XP:** {xp} / {xp_needed}"))
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) # Adding a separator
container.add_item(ui.TextDisplay(f"**Progress to Level {next_level}:**"))
container.add_item(ui.TextDisplay(f"[{bar}] {progress_percent}%"))
view = LevelCheckView(target, level, xp, xp_needed, next_level, bar, int(progress * 100))
await ctx.send(view=view)
@level.command(name="leaderboard", description="Show the server's level leaderboard")
async def leaderboard_command(self, ctx: commands.Context):
@ -401,22 +418,34 @@ class LevelingCog(commands.Cog):
# Sort by XP (descending)
sorted_data = sorted(guild_data.items(), key=lambda x: x[1]["xp"], reverse=True)
# Create embed
embed = discord.Embed(
title=f"{ctx.guild.name} Level Leaderboard",
color=discord.Color.gold()
)
class LeaderboardView(ui.LayoutView):
def __init__(self, guild_name: str, sorted_leaderboard_data: list, guild_members_dict: dict):
super().__init__()
# Add top 10 users to embed
for i, (user_id, data) in enumerate(sorted_data[:10], 1):
member = guild_members[user_id]
embed.add_field(
name=f"{i}. {member.display_name}",
value=f"Level: {data['level']} | XP: {data['xp']}",
inline=False
)
main_container = ui.Container(accent_colour=discord.Color.gold())
self.add_item(main_container)
await ctx.send(embed=embed)
main_container.add_item(ui.TextDisplay(f"**{guild_name} Level Leaderboard**"))
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
if not sorted_leaderboard_data:
main_container.add_item(ui.TextDisplay("The leaderboard is empty!"))
return
for i, (user_id, data) in enumerate(sorted_leaderboard_data[:10], 1):
member = guild_members_dict.get(user_id)
if not member:
continue
user_section = ui.Section()
user_section.add_item(ui.TextDisplay(f"**{i}. {member.display_name}**"))
user_section.add_item(ui.TextDisplay(f"Level: {data['level']} | XP: {data['xp']}"))
main_container.add_item(user_section)
if i < len(sorted_leaderboard_data[:10]): # Add separator between users, but not after the last one
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small, visible=False)) # Invisible separator for spacing
view = LeaderboardView(ctx.guild.name, sorted_data, guild_members)
await ctx.send(view=view)
@level.command(name="register_role", description="Register a role for a specific level")
@commands.has_permissions(manage_roles=True)
@ -469,24 +498,43 @@ class LevelingCog(commands.Cog):
await ctx.send("No level roles are registered for this server.")
return
embed = discord.Embed(
title=f"Level Roles for {ctx.guild.name}",
color=discord.Color.blue()
)
class ListLevelRolesView(ui.LayoutView):
def __init__(self, guild: discord.Guild, level_roles_data: dict):
super().__init__()
# Sort by level
sorted_roles = sorted(self.level_roles[ctx.guild.id].items())
main_container = ui.Container(accent_colour=discord.Color.blue())
self.add_item(main_container)
for level, role_id in sorted_roles:
role = ctx.guild.get_role(role_id)
role_name = role.mention if role else f"Unknown Role (ID: {role_id})"
embed.add_field(
name=f"Level {level}",
value=role_name,
inline=False
)
main_container.add_item(ui.TextDisplay(f"**Level Roles for {guild.name}**"))
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
await ctx.send(embed=embed)
if not level_roles_data: # Should be caught by the check above, but good practice
main_container.add_item(ui.TextDisplay("No level roles are registered for this server."))
return
sorted_roles_items = sorted(level_roles_data.items())
for level, role_data_or_id in sorted_roles_items:
role_section = ui.Section()
role_section.add_item(ui.TextDisplay(f"**Level {level}:**"))
if isinstance(role_data_or_id, dict): # Gendered roles
for gender, role_id in role_data_or_id.items():
role = guild.get_role(role_id)
role_name = role.mention if role else f"Unknown Role (ID: {role_id})"
role_section.add_item(ui.TextDisplay(f" - {gender.capitalize()}: {role_name}"))
else: # Regular role
role = guild.get_role(role_data_or_id)
role_name = role.mention if role else f"Unknown Role (ID: {role_data_or_id})"
role_section.add_item(ui.TextDisplay(f" {role_name}"))
main_container.add_item(role_section)
if level != sorted_roles_items[-1][0]: # Add separator if not the last item
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small, visible=False))
view = ListLevelRolesView(ctx.guild, self.level_roles[ctx.guild.id])
await ctx.send(view=view)
@level.command(name="restrict_channel", description="Restrict a channel from giving XP")
@commands.has_permissions(manage_channels=True)
@ -637,21 +685,49 @@ class LevelingCog(commands.Cog):
async def xp_config(self, ctx: commands.Context, setting: str = None, value: str = None):
"""Configure XP settings for the leveling system"""
if not setting:
# Display current settings
embed = discord.Embed(
title="XP Configuration Settings",
description="Current XP settings for the leveling system:",
color=discord.Color.blue()
)
class XPConfigView(ui.LayoutView):
def __init__(self, config_data: dict, prefix: str):
super().__init__()
embed.add_field(name="XP Per Message", value=str(self.config["xp_per_message"]), inline=True)
embed.add_field(name="XP Per Reaction", value=str(self.config["xp_per_reaction"]), inline=True)
embed.add_field(name="Message Cooldown", value=f"{self.config['message_cooldown']} seconds", inline=True)
embed.add_field(name="Reaction Cooldown", value=f"{self.config['reaction_cooldown']} seconds", inline=True)
embed.add_field(name="Reaction XP Enabled", value="Yes" if self.config["reaction_xp_enabled"] else "No", inline=True)
main_container = ui.Container(accent_colour=discord.Color.blue())
self.add_item(main_container)
embed.set_footer(text="Use !xp_config <setting> <value> to change a setting")
await ctx.send(embed=embed)
main_container.add_item(ui.TextDisplay("**XP Configuration Settings**"))
main_container.add_item(ui.TextDisplay("Current XP settings for the leveling system:"))
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
settings_to_display = [
("XP Per Message", str(config_data["xp_per_message"])),
("XP Per Reaction", str(config_data["xp_per_reaction"])),
("Message Cooldown", f"{config_data['message_cooldown']} seconds"),
("Reaction Cooldown", f"{config_data['reaction_cooldown']} seconds"),
("Reaction XP Enabled", "Yes" if config_data["reaction_xp_enabled"] else "No")
]
for name, value_str in settings_to_display:
setting_section = ui.Section()
setting_section.add_item(ui.TextDisplay(f"**{name}:** {value_str}"))
main_container.add_item(setting_section)
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
main_container.add_item(ui.TextDisplay(f"Use {prefix}level config <setting> <value> to change a setting")) # Updated help text
# Attempt to get the prefix
try:
# This is a common way to get the prefix, but might need adjustment
# depending on how the bot is structured (e.g., if get_prefix is an async method or part of bot instance)
# For simplicity, assuming ctx.prefix exists or can be hardcoded if necessary.
# If ctx.prefix is not available, a default like "!" or the bot's mention could be used.
# Let's assume ctx.prefix is available for now.
# If not, we might need to ask the user or make an assumption.
# For now, let's try with ctx.prefix. If it causes an error, we can adjust.
# A safer bet might be to use the command's qualified name.
command_prefix = ctx.prefix if ctx.prefix else "!" # Fallback to "!"
except AttributeError:
command_prefix = "!" # Fallback if ctx.prefix doesn't exist
view = XPConfigView(self.config, command_prefix)
await ctx.send(view=view)
return
if not value:
@ -889,28 +965,41 @@ class LevelingCog(commands.Cog):
# Save the updated level roles
self.save_level_roles()
# Update status message
created_str = "\n".join(created_roles) if created_roles else "None"
updated_str = "\n".join(updated_roles) if updated_roles else "None"
class MedievalRolesSetupView(ui.LayoutView):
def __init__(self, created_roles_list: list, updated_roles_list: list, has_pronoun_roles_flag: bool):
super().__init__()
embed = discord.Embed(
title="Medieval Level Roles Setup",
description="The following roles have been set up for the medieval leveling system:",
color=discord.Color.gold()
)
main_container = ui.Container(accent_colour=discord.Color.gold())
self.add_item(main_container)
if created_roles:
embed.add_field(name="Created Roles", value=created_str, inline=False)
if updated_roles:
embed.add_field(name="Updated Roles", value=updated_str, inline=False)
main_container.add_item(ui.TextDisplay("**Medieval Level Roles Setup**"))
main_container.add_item(ui.TextDisplay("The following roles have been set up for the medieval leveling system:"))
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
embed.add_field(
name="Gender Detection",
value="Gender-specific roles will be assigned based on pronoun roles." if has_pronoun_roles else "No pronoun roles detected. Using default titles.",
inline=False
)
if created_roles_list:
created_section = ui.Section()
created_section.add_item(ui.TextDisplay("**Created Roles:**"))
# For potentially long lists, join with newline. TextDisplay handles multiline.
created_section.add_item(ui.TextDisplay("\n".join(created_roles_list) if created_roles_list else "None"))
main_container.add_item(created_section)
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small, visible=False))
await status_message.edit(content=None, embed=embed)
if updated_roles_list:
updated_section = ui.Section()
updated_section.add_item(ui.TextDisplay("**Updated Roles:**"))
updated_section.add_item(ui.TextDisplay("\n".join(updated_roles_list) if updated_roles_list else "None"))
main_container.add_item(updated_section)
main_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small, visible=False))
gender_detection_section = ui.Section()
gender_detection_section.add_item(ui.TextDisplay("**Gender Detection:**"))
gender_text = "Gender-specific roles will be assigned based on pronoun roles." if has_pronoun_roles_flag else "No pronoun roles detected. Using default titles."
gender_detection_section.add_item(ui.TextDisplay(gender_text))
main_container.add_item(gender_detection_section)
view = MedievalRolesSetupView(created_roles, updated_roles, has_pronoun_roles)
await status_message.edit(content=None, view=view)
async def setup(bot: commands.Bot):
await bot.add_cog(LevelingCog(bot))