From 58b7e586d6eddc7747539bf1a126465a66c207ac Mon Sep 17 00:00:00 2001 From: Valerie Date: Mon, 26 May 2025 21:09:13 -0400 Subject: [PATCH] Update info.json to reflect new leveling system details, including author, description, and installation message. Modify pyproject.toml to remove Wand dependency. Enhance modern.py to generate SVG profile cards with improved stats presentation and avatar handling, streamlining profile generation process. --- info.json | 44 ++++- levelup/generator/styles/modern.py | 306 ++++++++++++++++++++--------- pyproject.toml | 1 - 3 files changed, 248 insertions(+), 103 deletions(-) diff --git a/info.json b/info.json index e2e3eb7..e86695b 100644 --- a/info.json +++ b/info.json @@ -1,16 +1,42 @@ { "author": [ + "Vertyco", "Valerie" ], - "install_msg": "Thanks for adding Ruby Cogs! Join our Discord @ https://discord.gg/5CA8sewarU", - "name": "Ruby Cogs", - "short": "Ruby Cogs, for our fork of Red-DiscordBot (Ruby-DiscordBot)", - "description": "Ruby Cogs, for our fork of Red-DiscordBot (Ruby-DiscordBot)", + "description": "Your friendly neighborhood leveling system", + "disabled": false, + "end_user_data_statement": "This cog stores Discord IDs, counts of user messages, and their time spent in voice channels. No private info is stored about users.", + "hidden": false, + "install_msg": "Thank you for installing LevelUp! To enable leveling in this server type `[p]lset toggle`.\n\nDOCUMENTATION: https://github.com/vertyco/vrt-cogs/blob/main/levelup/README.md", + "min_bot_version": "3.5.0", + "min_python_version": [ + 3, + 9, + 0 + ], + "permissions": [ + "read_messages", + "send_messages", + "manage_roles", + "attach_files", + "embed_links" + ], + "required_cogs": {}, + "requirements": [ + "pillow", + "svgwrite>=1.4.3", + "wand>=0.6.11" + ], + "short": "Discord leveling system", "tags": [ - "rubycogs", - "pokemon", + "activity", + "level", + "leveler", + "leveling", + "levelup", + "rank", "utility", - "information", - "various" - ] + "vert" + ], + "type": "COG" } \ No newline at end of file diff --git a/levelup/generator/styles/modern.py b/levelup/generator/styles/modern.py index 57e4390..4021e4c 100644 --- a/levelup/generator/styles/modern.py +++ b/levelup/generator/styles/modern.py @@ -1,7 +1,8 @@ import typing as t from pathlib import Path -from io import BytesIO +from io import BytesIO, StringIO import logging +import svgwrite from wand.image import Image from wand.drawing import Drawing from wand.color import Color @@ -14,6 +15,166 @@ except ImportError: log = logging.getLogger("red.vrt.levelup.generator.styles.modern") +def create_vector_profile( + width: int = 1050, + height: int = 320, + avatar_size: int = 200, + username: str = "", + level: int = 1, + messages: int = 0, + voicetime: int = 0, + stars: int = 0, + prestige: int = 0, + balance: t.Optional[int] = None, + currency_name: str = "", + progress: float = 0, + current_xp: int = 0, + next_xp: int = 0, + position: int = 0, + user_color: t.Tuple[int, int, int] = (88, 101, 242), + stat_color: t.Tuple[int, int, int] = (255, 255, 255), + level_bar_color: t.Tuple[int, int, int] = (88, 101, 242), +) -> str: + """Generate SVG profile card""" + # Create SVG document + dwg = svgwrite.Drawing(size=(width, height)) + + # Define gradients + # Main background gradient + grad = dwg.defs.add(dwgwrite.gradients.LinearGradient(id="background")) + grad.add_stop_color(0, "rgb(20, 20, 30)") + grad.add_stop_color(1, "rgb(40, 40, 60)") + + # Progress bar gradient + prog_grad = dwg.defs.add(dwgwrite.gradients.LinearGradient(id="progress", x1="0%", y1="0%", x2="0%", y2="100%")) + r, g, b = level_bar_color + prog_grad.add_stop_color(0, f"rgb({r}, {g}, {b})") + prog_grad.add_stop_color(0.5, f"rgb({min(r+20, 255)}, {min(g+20, 255)}, {min(b+20, 255)})") + prog_grad.add_stop_color(1, f"rgb({r}, {g}, {b})") + + # Add background + dwg.add(dwg.rect(insert=(0, 0), size=(width, height), fill="url(#background)")) + + # Add glass panel with blur effect (using SVG filters) + blur = dwg.defs.add(dwg.filter(id="blur")) + blur.feGaussianBlur(in_="SourceGraphic", stdDeviation=8) + glass = dwg.add(dwg.rect( + insert=(15, 15), + size=(width-30, height-30), + rx=25, ry=25, + fill="rgba(255, 255, 255, 0.1)", + filter="url(#blur)", + stroke="rgba(255, 255, 255, 0.2)", + stroke_width=1 + )) + + # Add stats container + stats = dwg.add(dwg.g(id="stats", transform=f"translate({avatar_size + 80}, 40)")) + + # Add level text with glow effect + glow = dwg.defs.add(dwg.filter(id="glow")) + glow.feGaussianBlur(in_="SourceAlpha", stdDeviation=2) + glow.feOffset(dx=0, dy=0) + glow.feComponentTransfer().feFuncA(type="linear", slope=0.5) + glow.feBlend(in2="SourceGraphic", mode="normal") + + level_text = f"LEVEL {level}" + if prestige > 0: + level_text = f"P{prestige} • {level_text}" + + r, g, b = user_color + stats.add(dwg.text( + level_text, + insert=(0, 0), + fill=f"rgb({r}, {g}, {b})", + filter="url(#glow)", + style="font-size:56px; font-weight:bold; font-family:Arial" + )) + + # Add stats with vector icons + def add_stat(x: int, y: int, icon: str, label: str, value: str): + group = dwg.g(transform=f"translate({x}, {y})") + # Add icon + group.add(dwg.text( + icon, + insert=(0, 0), + fill="white", + style="font-size:28px; font-family:Arial" + )) + # Add label + group.add(dwg.text( + f"{label}:", + insert=(40, 0), + fill="rgb(220, 220, 220)", + style="font-size:28px; font-family:Arial" + )) + # Add value with glow + group.add(dwg.text( + value, + insert=(140, 0), + fill="white", + filter="url(#glow)", + style="font-size:32px; font-weight:bold; font-family:Arial" + )) + return group + + # Left column stats + stats.add(add_stat(0, 80, "💬", "Messages", humanize_number(messages))) + stats.add(add_stat(0, 130, "🎤", "Voice", imgtools.abbreviate_time(voicetime))) + stats.add(add_stat(0, 180, "⭐", "Stars", humanize_number(stars))) + + # Right column stats + if balance is not None: + stats.add(add_stat(320, 80, "💰", "Balance", f"{humanize_number(balance)} {currency_name}")) + stats.add(add_stat(320, 130, "🏆", "Rank", f"#{humanize_number(position)}")) + + # Progress bar + bar_width = 620 + bar_height = 30 + bar_x = 0 + bar_y = 220 + + # Progress bar background + stats.add(dwg.rect( + insert=(bar_x, bar_y), + size=(bar_width, bar_height), + rx=bar_height//2, + ry=bar_height//2, + fill="rgba(0, 0, 0, 0.3)" + )) + + # Progress bar fill + progress_width = int(bar_width * progress) + if progress_width > 0: + stats.add(dwg.rect( + insert=(bar_x, bar_y), + size=(progress_width, bar_height), + rx=bar_height//2, + ry=bar_height//2, + fill="url(#progress)" + )) + + # XP Text + xp_text = f"XP: {humanize_number(current_xp)} / {humanize_number(next_xp)}" + stats.add(dwg.text( + xp_text, + insert=(bar_x + (bar_width - len(xp_text) * 10) / 2, bar_y - 15), + fill="white", + style="font-size:20px; font-family:Arial" + )) + + # Progress percentage + percent_text = f"{int(progress * 100)}%" + stats.add(dwg.text( + percent_text, + insert=(bar_x + progress_width - 30, bar_y + 20), + fill="white", + style="font-size:24px; font-weight:bold; font-family:Arial" + )) + + # Return SVG as string + return dwg.tostring() + def generate_modern_profile( background_bytes: t.Optional[t.Union[bytes, str]] = None, avatar_bytes: t.Optional[t.Union[bytes, str]] = None, @@ -42,16 +203,38 @@ def generate_modern_profile( debug: bool = False, **kwargs, ) -> t.Tuple[bytes, bool]: - """Generate a modern, sleek profile card using Wand/ImageMagick""" + """Generate a modern, sleek profile card using vector graphics""" # Set default colors for modern theme user_color = user_color or (88, 101, 242) # Discord Blurple stat_color = stat_color or (255, 255, 255) # White level_bar_color = level_bar_color or (88, 101, 242) # Discord Blurple - # Create base canvas (1050x320) + # Calculate progress + progress = (current_xp - previous_xp) / (next_xp - previous_xp) + + # Generate SVG + svg_data = create_vector_profile( + username=username, + level=level, + messages=messages, + voicetime=voicetime, + stars=stars, + prestige=prestige, + balance=balance, + currency_name=currency_name, + progress=progress, + current_xp=current_xp, + next_xp=next_xp, + position=position, + user_color=user_color, + stat_color=stat_color, + level_bar_color=level_bar_color, + ) + + # Convert SVG to image with Image(width=1050, height=320) as card: - # Set up background + # Set up background if provided if background_bytes: with Image(blob=background_bytes) as bg: bg.transform(resize='1050x320^') @@ -60,98 +243,35 @@ def generate_modern_profile( bg.gaussian_blur(sigma=15) card.composite(bg, 0, 0) else: - # Create gradient background + # Use gradient background card.gradient('linear-gradient', 'navy-darkblue') - # Add frosted glass effect overlay - with Image(width=1050, height=320, background=Color('rgba(255, 255, 255, 0.1)')) as overlay: - overlay.gaussian_blur(sigma=50) - card.composite(overlay, 0, 0) - - # Create circular avatar mask - with Drawing() as draw: - # Avatar circle (left side) - avatar_size = 200 - avatar_x = 60 - avatar_y = 60 - - if avatar_bytes: - with Image(blob=avatar_bytes) as avatar: - # Resize avatar - avatar.resize(avatar_size, avatar_size) - # Create circular mask - with Image(width=avatar_size, height=avatar_size, background=Color('transparent')) as mask: - draw.circle((avatar_size/2, avatar_size/2), (0, avatar_size/2)) - draw.draw(mask) - # Apply mask to avatar - avatar.composite_channel('alpha', mask, 'copy_alpha', 0, 0) - # Add avatar to card - card.composite(avatar, avatar_x, avatar_y) - - # Add modern stats panel - with Image(width=700, height=280, background=Color('rgba(0, 0, 0, 0.5)')) as stats_panel: - stats_panel.gaussian_blur(sigma=2) - stats_panel.border_color = Color('rgba(255, 255, 255, 0.1)') - stats_panel.border_width = 1 - card.composite(stats_panel, 310, 20) - - # Add text elements - draw.font = str(font_path) if font_path else '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf' - draw.font_size = 40 - draw.fill_color = Color('rgb({}, {}, {})'.format(*user_color)) - - # Level text - level_text = f"LEVEL {level}" - if prestige > 0: - level_text = f"P{prestige} • {level_text}" - draw.text(340, 70, level_text) - - # Stats - draw.font_size = 25 - draw.fill_color = Color('white') - y_pos = 120 - - # Left column - draw.text(340, y_pos, f"Messages: {humanize_number(messages)}") - draw.text(340, y_pos + 40, f"Voice Time: {imgtools.abbreviate_time(voicetime)}") - draw.text(340, y_pos + 80, f"Stars: {humanize_number(stars)}") - - # Right column - if balance is not None: - draw.text(600, y_pos, f"Balance: {humanize_number(balance)} {currency_name}") - draw.text(600, y_pos + 40, f"Rank: #{humanize_number(position)}") - - # Progress bar - progress = (current_xp - previous_xp) / (next_xp - previous_xp) - bar_width = 620 - bar_height = 30 - bar_x = 340 - bar_y = 240 - - # Background bar - draw.fill_color = Color('rgba(0, 0, 0, 0.3)') - draw.rectangle(bar_x, bar_y, bar_x + bar_width, bar_y + bar_height) - - # Progress bar - progress_width = int(bar_width * progress) - draw.fill_color = Color('rgb({}, {}, {})'.format(*level_bar_color)) - if progress_width > 0: - draw.rectangle(bar_x, bar_y, bar_x + progress_width, bar_y + bar_height) - - # XP Text - draw.font_size = 20 - draw.fill_color = Color('white') - xp_text = f"XP: {humanize_number(current_xp)} / {humanize_number(next_xp)}" - draw.text(bar_x + (bar_width - len(xp_text) * 10) / 2, bar_y - 15, xp_text) - - # Progress percentage - percent_text = f"{int(progress * 100)}%" - draw.text(bar_x + progress_width - 30, bar_y + 5, percent_text) - - # Apply all drawings - draw.draw(card) + # Add SVG elements + with Image(blob=svg_data.encode(), format='svg') as svg: + card.composite(svg, 0, 0) - # Save the result + # Add avatar if provided + if avatar_bytes: + with Image(blob=avatar_bytes) as avatar: + # Create circular avatar + avatar.resize(200, 200) + avatar.virtual_pixel = 'transparent' + avatar.format = 'png' + avatar.alpha_channel = True + + # Create circular mask + with Image(width=200, height=200, background=Color('transparent')) as mask: + mask.format = 'png' + with Drawing() as draw: + draw.fill_color = Color('white') + draw.circle((100, 100), (0, 100)) + draw.draw(mask) + avatar.composite_channel('alpha', mask, 'copy_alpha', 0, 0) + + # Add avatar to card + card.composite(avatar, 60, 60) + + # Save as WebP card.format = 'webp' card.compression_quality = 95 diff --git a/pyproject.toml b/pyproject.toml index 92fe79a..1e98cd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,4 +10,3 @@ [tool.poetry.dependencies] python = ">=3.8.1,<4.0" -Wand = "^0.6.11" # ImageMagick Python bindings for modern image processing