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