From 719abfbc970f78ca62ded57bb753bbfea07f0dd8 Mon Sep 17 00:00:00 2001 From: Valerie Date: Mon, 26 May 2025 04:11:47 -0400 Subject: [PATCH] Implement interactive leaderboard navigation in Leaderboard cog. Introduce a new LeaderboardView class for paginated display of leaderboard entries, enhancing user experience with button controls for navigation. Update leaderboard command to utilize embeds for better formatting and clarity, and improve user feedback for points checking. --- leaderboard/leaderboard.py | 143 +++++++++++++++++++++++++++++-------- levelup/shared/profile.py | 4 +- 2 files changed, 115 insertions(+), 32 deletions(-) diff --git a/leaderboard/leaderboard.py b/leaderboard/leaderboard.py index 076f60c..cafb4dc 100644 --- a/leaderboard/leaderboard.py +++ b/leaderboard/leaderboard.py @@ -1,14 +1,75 @@ from redbot.core import commands, Config from redbot.core.bot import Red -from redbot.core.utils.chat_formatting import box, pagify +from redbot.core.utils.chat_formatting import box, pagify, humanize_number +import discord +from discord.ui import Button, View import aiohttp import asyncio import logging from datetime import datetime, timezone -from typing import Dict, Optional +from typing import Dict, Optional, List log = logging.getLogger("red.ruby.leaderboard") +class LeaderboardView(View): + def __init__(self, cog, ctx, pages: List[discord.Embed], timeout: float = 180.0): + super().__init__(timeout=timeout) + self.cog = cog + self.ctx = ctx + self.pages = pages + self.current_page = 0 + self.message = None + + # Add buttons + self.first_page.disabled = True + self.prev_page.disabled = True + if len(self.pages) == 1: + self.next_page.disabled = True + self.last_page.disabled = True + + @discord.ui.button(label="≪", style=discord.ButtonStyle.gray) + async def first_page(self, interaction: discord.Interaction, button: discord.ui.Button): + self.current_page = 0 + await self.update_page(interaction) + + @discord.ui.button(label="Previous", style=discord.ButtonStyle.blurple) + async def prev_page(self, interaction: discord.Interaction, button: discord.ui.Button): + self.current_page = max(0, self.current_page - 1) + await self.update_page(interaction) + + @discord.ui.button(label="Next", style=discord.ButtonStyle.blurple) + async def next_page(self, interaction: discord.Interaction, button: discord.ui.Button): + self.current_page = min(len(self.pages) - 1, self.current_page + 1) + await self.update_page(interaction) + + @discord.ui.button(label="≫", style=discord.ButtonStyle.gray) + async def last_page(self, interaction: discord.Interaction, button: discord.ui.Button): + self.current_page = len(self.pages) - 1 + await self.update_page(interaction) + + async def update_page(self, interaction: discord.Interaction): + # Update button states + self.first_page.disabled = self.current_page == 0 + self.prev_page.disabled = self.current_page == 0 + self.next_page.disabled = self.current_page == len(self.pages) - 1 + self.last_page.disabled = self.current_page == len(self.pages) - 1 + + await interaction.response.edit_message(embed=self.pages[self.current_page], view=self) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id == self.ctx.author.id: + return True + await interaction.response.send_message("This menu is not for you!", ephemeral=True) + return False + + async def on_timeout(self): + try: + for item in self.children: + item.disabled = True + await self.message.edit(view=self) + except discord.HTTPException: + pass + class Leaderboard(commands.Cog): """Global leaderboard system for Ruby.""" @@ -134,8 +195,7 @@ class Leaderboard(commands.Cog): async def globalboard(self, ctx: commands.Context): """Global leaderboard commands.""" if ctx.invoked_subcommand is None: - # Don't send help here, just show the base command response - await ctx.send("Use `!help globalboard` to see available commands.") + await self.show_leaderboard(ctx) @globalboard.command(name="show") async def show_leaderboard(self, ctx: commands.Context, page: int = 1): @@ -152,34 +212,39 @@ class Leaderboard(commands.Cog): return await ctx.send("The leaderboard is currently empty!") items_per_page = 10 - pages = [leaderboard_data[i:i + items_per_page] + chunks = [leaderboard_data[i:i + items_per_page] for i in range(0, len(leaderboard_data), items_per_page)] - if not pages: # No data after pagination + if not chunks: # No data after pagination return await ctx.send("The leaderboard is currently empty!") + + embeds = [] + for page_num, entries in enumerate(chunks, 1): + embed = discord.Embed( + title="🏆 Global Leaderboard", + color=await ctx.embed_color() + ) - if not 1 <= page <= len(pages): - return await ctx.send(f"Invalid page number. Please choose between 1 and {len(pages)}.") - - entries = pages[page - 1] - - # Format leaderboard - lines = [] - start_pos = (page - 1) * items_per_page - - for i, entry in enumerate(entries, start=start_pos + 1): - username = entry.get("username", "Unknown User") - points = entry.get("points", 0) - lines.append(f"{i}. {username}: {points:,} points") - - if not lines: # No valid entries - return await ctx.send("No valid leaderboard entries found.") + # Format leaderboard entries + description = [] + start_pos = (page_num - 1) * items_per_page - header = f"🏆 Global Leaderboard (Page {page}/{len(pages)})" - footer = f"Use {ctx.prefix}globalboard show to view other pages" - - content = box("\n".join([header, *lines, "", footer]), lang="md") - await ctx.send(content) + for i, entry in enumerate(entries, start=start_pos + 1): + username = entry.get("username", "Unknown User") + user_id = entry.get("userId", "0") + points = entry.get("points", 0) + + # Format each entry with position, name, points, and user ID + description.append( + f"`{i}.` <@{user_id}> • **{humanize_number(points)}** points" + ) + + embed.description = "\n".join(description) + embed.set_footer(text=f"Page {page_num}/{len(chunks)} • Total Users: {len(leaderboard_data)}") + embeds.append(embed) + + view = LeaderboardView(self, ctx, embeds) + view.message = await ctx.send(embed=embeds[0], view=view) @globalboard.command(name="points") async def check_points(self, ctx: commands.Context, member: commands.MemberConverter = None): @@ -203,12 +268,28 @@ class Leaderboard(commands.Cog): if entry.get("userId") == str(member.id)), None ) - await ctx.send( - f"🏆 **{member.display_name}** has **{user_data.get('points', 0):,}** points " - f"(Rank: #{rank:,})" + + embed = discord.Embed( + title="🏆 Global Points", + color=await ctx.embed_color() ) + embed.description = ( + f"**User:** <@{member.id}>\n" + f"**Points:** {humanize_number(user_data.get('points', 0))}\n" + f"**Rank:** #{humanize_number(rank)}\n" + f"**ID:** {member.id}" + ) + embed.set_thumbnail(url=member.display_avatar.url) + + await ctx.send(embed=embed) else: - await ctx.send(f"**{member.display_name}** has no points yet!") + embed = discord.Embed( + title="No Points Found", + description=f"<@{member.id}> has no points yet!", + color=await ctx.embed_color() + ) + embed.set_footer(text=f"ID: {member.id}") + await ctx.send(embed=embed) @commands.Cog.listener() async def on_message(self, message): diff --git a/levelup/shared/profile.py b/levelup/shared/profile.py index ee447e6..21e9b07 100644 --- a/levelup/shared/profile.py +++ b/levelup/shared/profile.py @@ -175,7 +175,9 @@ class ProfileFormatting(MixinMeta): # Add server score/rank info if stat is not None: - description.append(f"Server Score: **{humanize_number(current_xp)}** (Rank #{stat})") + description.append(f"Server Score: **{humanize_number(current_xp)}** XP") + if isinstance(stat, dict) and "position" in stat: + description.append(f"Rank: **#{humanize_number(stat['position'])}**") # Add level progress bar progress_perc = (progress / current_diff) * 100