Files
disbord/ui/utils.py
Travis Vasceannie 3acb779569 chore: remove .env.example and add new files for project structure
- Deleted .env.example file as it is no longer needed.
- Added .gitignore to manage ignored files and directories.
- Introduced CLAUDE.md for AI provider integration documentation.
- Created dev.sh for development setup and scripts.
- Updated Dockerfile and Dockerfile.production for improved build processes.
- Added multiple test files and directories for comprehensive testing.
- Introduced new utility and service files for enhanced functionality.
- Organized codebase with new directories and files for better maintainability.
2025-08-27 23:00:19 -04:00

352 lines
10 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
UI Utilities for Discord Voice Chat Quote Bot
Provides utility functions for consistent Discord UI formatting,
styling, and component creation across the bot.
"""
from datetime import datetime
from typing import Any, TypeVar
import discord
T = TypeVar("T")
class EmbedStyles:
"""Standard embed color schemes and styles"""
# Color palette
PRIMARY = 0x3498DB # Blue
SUCCESS = 0x2ECC71 # Green
WARNING = 0xF39C12 # Orange
DANGER = 0xE74C3C # Red
INFO = 0x9B59B6 # Purple
SECONDARY = 0x95A5A6 # Gray
# Quote category colors
FUNNY = 0xFFEB3B # Yellow
DARK = 0x424242 # Dark gray
SILLY = 0xFF9800 # Orange
SUSPICIOUS = 0xFF5722 # Deep orange
ASININE = 0x9C27B0 # Purple
class UIFormatter:
"""Utility functions for formatting UI elements"""
@staticmethod
def format_score_bar(
score: float, max_score: float = 10.0, length: int = 10
) -> str:
"""Create a visual score bar using Unicode blocks"""
if score <= 0:
return "" * length
filled = int((score / max_score) * length)
filled = max(0, min(filled, length))
return "" * filled + "" * (length - filled)
@staticmethod
def format_percentage_bar(percentage: float, length: int = 10) -> str:
"""Create a percentage bar"""
filled = int((percentage / 100.0) * length)
filled = max(0, min(filled, length))
return "" * filled + "" * (length - filled)
@staticmethod
def format_timestamp(timestamp: datetime, style: str = "relative") -> str:
"""Format timestamp for Discord display"""
unix_timestamp = int(timestamp.timestamp())
if style == "relative":
return f"<t:{unix_timestamp}:R>"
elif style == "short":
return f"<t:{unix_timestamp}:d>"
elif style == "long":
return f"<t:{unix_timestamp}:F>"
else:
return timestamp.strftime("%Y-%m-%d %H:%M")
@staticmethod
def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str:
"""Truncate text with ellipsis"""
if len(text) <= max_length:
return text
return text[: max_length - len(suffix)] + suffix
@staticmethod
def format_score_emojis(scores: dict[str, float], threshold: float = 5.0) -> str:
"""Format score dictionary into emoji representation"""
emoji_map = {
"funny_score": "😂",
"dark_score": "🖤",
"silly_score": "🤪",
"suspicious_score": "🤔",
"asinine_score": "🙄",
}
formatted_scores = []
for score_type, score in scores.items():
if score > threshold and score_type in emoji_map:
emoji = emoji_map[score_type]
formatted_scores.append(f"{emoji} {score:.1f}")
return (
" | ".join(formatted_scores)
if formatted_scores
else "No significant scores"
)
@staticmethod
def create_progress_indicator(current: int, total: int) -> str:
"""Create a progress indicator"""
if total == 0:
return "0/0"
percentage = int((current / total) * 100)
return f"{current}/{total} ({percentage}%)"
@staticmethod
def format_duration(seconds: float) -> str:
"""Format duration in human-readable format"""
if seconds < 60:
return f"{seconds:.1f}s"
elif seconds < 3600:
minutes = int(seconds // 60)
remaining_seconds = int(seconds % 60)
return f"{minutes}m {remaining_seconds}s"
else:
hours = int(seconds // 3600)
remaining_minutes = int((seconds % 3600) // 60)
return f"{hours}h {remaining_minutes}m"
class EmbedBuilder:
"""Enhanced embed builder with consistent styling"""
@staticmethod
def create_error_embed(
title: str, description: str, details: str | None = None
) -> discord.Embed:
"""Create standardized error embed"""
embed = discord.Embed(
title=f"{title}", description=description, color=EmbedStyles.DANGER
)
if details:
embed.add_field(name="Details", value=details, inline=False)
return embed
@staticmethod
def create_success_embed(
title: str, description: str, details: str | None = None
) -> discord.Embed:
"""Create standardized success embed"""
embed = discord.Embed(
title=f"{title}", description=description, color=EmbedStyles.SUCCESS
)
if details:
embed.add_field(name="Details", value=details, inline=False)
return embed
@staticmethod
def create_info_embed(
title: str, description: str, color: int = EmbedStyles.INFO
) -> discord.Embed:
"""Create standardized info embed"""
return discord.Embed(title=f" {title}", description=description, color=color)
@staticmethod
def create_warning_embed(
title: str, description: str, warning: str | None = None
) -> discord.Embed:
"""Create standardized warning embed"""
embed = discord.Embed(
title=f"⚠️ {title}", description=description, color=EmbedStyles.WARNING
)
if warning:
embed.add_field(name="Warning", value=warning, inline=False)
return embed
@staticmethod
def create_loading_embed(operation: str) -> discord.Embed:
"""Create loading/processing embed"""
return discord.Embed(
title="⏳ Processing",
description=f"Please wait while we {operation}...",
color=EmbedStyles.INFO,
)
class PaginationHelper:
"""Helper functions for pagination UI"""
@staticmethod
def calculate_pages(total_items: int, items_per_page: int) -> int:
"""Calculate total number of pages"""
if total_items == 0:
return 1
return (total_items + items_per_page - 1) // items_per_page
@staticmethod
def get_page_items(items: list[T], page: int, items_per_page: int) -> list[T]:
"""Get items for a specific page"""
start_idx = page * items_per_page
end_idx = start_idx + items_per_page
return items[start_idx:end_idx]
@staticmethod
def create_page_info(current_page: int, total_pages: int, total_items: int) -> str:
"""Create page information string"""
return f"Page {current_page + 1} of {total_pages}{total_items} total items"
class ValidationHelper:
"""Helper functions for input validation"""
@staticmethod
def validate_quote_text(
text: str, min_length: int = 3, max_length: int = 1000
) -> tuple:
"""Validate quote text input"""
if not text or not text.strip():
return False, "Quote text cannot be empty"
text = text.strip()
if len(text) < min_length:
return False, f"Quote must be at least {min_length} characters long"
if len(text) > max_length:
return False, f"Quote must be no more than {max_length} characters long"
return True, text
@staticmethod
def validate_search_query(
query: str, min_length: int = 2, max_length: int = 100
) -> tuple:
"""Validate search query input"""
if not query or not query.strip():
return False, "Search query cannot be empty"
query = query.strip()
if len(query) < min_length:
return False, f"Search query must be at least {min_length} characters long"
if len(query) > max_length:
return (
False,
f"Search query must be no more than {max_length} characters long",
)
return True, query
@staticmethod
def sanitize_user_input(text: str) -> str:
"""Sanitize user input for safe display"""
# Remove potential markdown that could break formatting
text = text.replace("```", "'''")
text = text.replace("**", "\\*\\*")
text = text.replace("__", "\\_\\_")
text = text.replace("~~", "\\~\\~")
return text
class StatusIndicators:
"""Standard status indicators and emojis"""
# Status emojis
ONLINE = "🟢"
IDLE = "🟡"
DND = "🔴"
OFFLINE = ""
# Feature status
ENABLED = ""
DISABLED = ""
PENDING = ""
WARNING = "⚠️"
# Categories
FUNNY = "😂"
DARK = "🖤"
SILLY = "🤪"
SUSPICIOUS = "🤔"
ASININE = "🙄"
# Actions
PLAY = "▶️"
PAUSE = "⏸️"
STOP = "⏹️"
RECORD = "🎤"
MUTE = "🔇"
UNMUTE = "🔊"
@staticmethod
def get_health_indicator(healthy: bool) -> str:
"""Get health status indicator"""
return StatusIndicators.ENABLED if healthy else StatusIndicators.DISABLED
@staticmethod
def get_score_emoji(score_type: str) -> str:
"""Get emoji for score type"""
emoji_map = {
"funny": StatusIndicators.FUNNY,
"dark": StatusIndicators.DARK,
"silly": StatusIndicators.SILLY,
"suspicious": StatusIndicators.SUSPICIOUS,
"asinine": StatusIndicators.ASININE,
}
return emoji_map.get(score_type.lower(), "📊")
def create_field_list(
items: list[str],
max_fields: int = 25,
field_name: str = "Items",
inline: bool = True,
) -> list[dict[str, Any]]:
"""
Create list of embed fields for large item lists.
Args:
items: List of items to display
max_fields: Maximum number of fields per embed
field_name: Base name for fields
inline: Whether fields should be inline
Returns:
List of field dictionaries
"""
fields = []
items_per_field = 10 # Discord embed field value limit considerations
for i in range(0, len(items), items_per_field):
field_items = items[i : i + items_per_field]
field_value = "\n".join([f"{item}" for item in field_items])
field_num = (i // items_per_field) + 1
field_title = (
f"{field_name}"
if len(items) <= items_per_field
else f"{field_name} ({field_num})"
)
fields.append({"name": field_title, "value": field_value, "inline": inline})
if len(fields) >= max_fields:
break
return fields