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