aaa
This commit is contained in:
parent
0c4f00d747
commit
4b50898664
@ -28,6 +28,15 @@ class Conversation(BaseModel):
|
||||
web_search_enabled: bool = False
|
||||
system_message: Optional[str] = None
|
||||
|
||||
class ThemeSettings(BaseModel):
|
||||
"""Theme settings for the dashboard UI"""
|
||||
theme_mode: str = "light" # "light", "dark", "custom"
|
||||
primary_color: str = "#5865F2" # Discord blue
|
||||
secondary_color: str = "#2D3748"
|
||||
accent_color: str = "#7289DA"
|
||||
font_family: str = "Inter, sans-serif"
|
||||
custom_css: Optional[str] = None
|
||||
|
||||
class UserSettings(BaseModel):
|
||||
# General settings
|
||||
model_id: str = "openai/gpt-3.5-turbo"
|
||||
@ -54,6 +63,9 @@ class UserSettings(BaseModel):
|
||||
advanced_view_enabled: bool = False
|
||||
streaming_enabled: bool = True
|
||||
|
||||
# Theme settings
|
||||
theme: ThemeSettings = Field(default_factory=ThemeSettings)
|
||||
|
||||
# Last updated timestamp
|
||||
last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||
|
||||
|
@ -691,14 +691,19 @@ class CommandPermission(BaseModel):
|
||||
class CommandPermissionsResponse(BaseModel):
|
||||
permissions: Dict[str, List[str]] # Command name -> List of allowed role IDs
|
||||
|
||||
class CommandCustomizationDetail(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class CommandCustomizationResponse(BaseModel):
|
||||
command_customizations: Dict[str, str] = {} # Original command name -> Custom command name
|
||||
command_customizations: Dict[str, Dict[str, Optional[str]]] = {} # Original command name -> {name, description}
|
||||
group_customizations: Dict[str, str] = {} # Original group name -> Custom group name
|
||||
command_aliases: Dict[str, List[str]] = {} # Original command name -> List of aliases
|
||||
|
||||
class CommandCustomizationUpdate(BaseModel):
|
||||
command_name: str
|
||||
custom_name: Optional[str] = None # If None, removes customization
|
||||
custom_description: Optional[str] = None # If None, keeps existing or no description
|
||||
|
||||
class GroupCustomizationUpdate(BaseModel):
|
||||
group_name: str
|
||||
|
@ -88,8 +88,16 @@ async def get_command_customizations(
|
||||
detail="Failed to get command aliases"
|
||||
)
|
||||
|
||||
# Convert command_customizations to the new format
|
||||
formatted_command_customizations = {}
|
||||
for cmd_name, cmd_data in command_customizations.items():
|
||||
formatted_command_customizations[cmd_name] = {
|
||||
'name': cmd_data.get('name', cmd_name),
|
||||
'description': cmd_data.get('description')
|
||||
}
|
||||
|
||||
return CommandCustomizationResponse(
|
||||
command_customizations=command_customizations,
|
||||
command_customizations=formatted_command_customizations,
|
||||
group_customizations=group_customizations,
|
||||
command_aliases=command_aliases
|
||||
)
|
||||
@ -110,7 +118,7 @@ async def set_command_customization(
|
||||
_user: dict = Depends(get_dashboard_user),
|
||||
_admin: bool = Depends(verify_dashboard_guild_admin)
|
||||
):
|
||||
"""Set a custom name for a command in a guild."""
|
||||
"""Set a custom name and/or description for a command in a guild."""
|
||||
try:
|
||||
# Check if settings_manager is available
|
||||
if not settings_manager or not settings_manager.pg_pool:
|
||||
@ -133,19 +141,41 @@ async def set_command_customization(
|
||||
detail="Custom command names must be between 1 and 32 characters long"
|
||||
)
|
||||
|
||||
# Validate custom description if provided
|
||||
if customization.custom_description is not None:
|
||||
if len(customization.custom_description) > 100:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Custom command descriptions must be 100 characters or less"
|
||||
)
|
||||
|
||||
# Set the custom command name
|
||||
success = await settings_manager.set_custom_command_name(
|
||||
name_success = await settings_manager.set_custom_command_name(
|
||||
guild_id,
|
||||
customization.command_name,
|
||||
customization.custom_name
|
||||
)
|
||||
|
||||
if not success:
|
||||
if not name_success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to set custom command name"
|
||||
)
|
||||
|
||||
# Set the custom command description if provided
|
||||
if customization.custom_description is not None:
|
||||
desc_success = await settings_manager.set_custom_command_description(
|
||||
guild_id,
|
||||
customization.command_name,
|
||||
customization.custom_description
|
||||
)
|
||||
|
||||
if not desc_success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to set custom command description"
|
||||
)
|
||||
|
||||
return {"message": "Command customization updated successfully"}
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
|
@ -8,6 +8,9 @@ from typing import List, Dict, Optional, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Body
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Default prefix for commands
|
||||
DEFAULT_PREFIX = "!"
|
||||
|
||||
# Import the dependencies from api_server.py
|
||||
try:
|
||||
# Try relative import first
|
||||
@ -78,6 +81,14 @@ class Message(BaseModel):
|
||||
role: str # 'user' or 'assistant'
|
||||
created_at: str
|
||||
|
||||
class ThemeSettings(BaseModel):
|
||||
theme_mode: str = "light" # "light", "dark", "custom"
|
||||
primary_color: str = "#5865F2" # Discord blue
|
||||
secondary_color: str = "#2D3748"
|
||||
accent_color: str = "#7289DA"
|
||||
font_family: str = "Inter, sans-serif"
|
||||
custom_css: Optional[str] = None
|
||||
|
||||
class GlobalSettings(BaseModel):
|
||||
system_message: Optional[str] = None
|
||||
character: Optional[str] = None
|
||||
@ -86,6 +97,19 @@ class GlobalSettings(BaseModel):
|
||||
model: Optional[str] = None
|
||||
temperature: Optional[float] = None
|
||||
max_tokens: Optional[int] = None
|
||||
theme: Optional[ThemeSettings] = None
|
||||
|
||||
class CogInfo(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
enabled: bool = True
|
||||
commands: List[Dict[str, Any]] = []
|
||||
|
||||
class CommandInfo(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
enabled: bool = True
|
||||
cog_name: Optional[str] = None
|
||||
|
||||
# --- Endpoints ---
|
||||
@router.get("/guilds/{guild_id}/channels", response_model=List[Channel])
|
||||
@ -424,6 +448,58 @@ async def remove_command_alias(
|
||||
detail=f"Error removing command alias: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/guilds/{guild_id}/settings", response_model=Dict[str, Any])
|
||||
async def get_guild_settings(
|
||||
guild_id: int,
|
||||
_user: dict = Depends(get_dashboard_user),
|
||||
_admin: bool = Depends(verify_dashboard_guild_admin)
|
||||
):
|
||||
"""Get settings for a guild."""
|
||||
try:
|
||||
# Check if settings_manager is available
|
||||
if not settings_manager or not settings_manager.pg_pool:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Settings manager not available"
|
||||
)
|
||||
|
||||
# Get prefix
|
||||
prefix = await settings_manager.get_guild_prefix(guild_id, DEFAULT_PREFIX)
|
||||
|
||||
# Get welcome/goodbye settings
|
||||
welcome_channel_id = await settings_manager.get_setting(guild_id, 'welcome_channel_id')
|
||||
welcome_message = await settings_manager.get_setting(guild_id, 'welcome_message')
|
||||
goodbye_channel_id = await settings_manager.get_setting(guild_id, 'goodbye_channel_id')
|
||||
goodbye_message = await settings_manager.get_setting(guild_id, 'goodbye_message')
|
||||
|
||||
# Get cog enabled statuses
|
||||
cogs_enabled = await settings_manager.get_all_enabled_cogs(guild_id)
|
||||
|
||||
# Get command enabled statuses
|
||||
commands_enabled = await settings_manager.get_all_enabled_commands(guild_id)
|
||||
|
||||
# Construct response
|
||||
settings = {
|
||||
"prefix": prefix,
|
||||
"welcome_channel_id": welcome_channel_id,
|
||||
"welcome_message": welcome_message,
|
||||
"goodbye_channel_id": goodbye_channel_id,
|
||||
"goodbye_message": goodbye_message,
|
||||
"cogs": cogs_enabled,
|
||||
"commands": commands_enabled
|
||||
}
|
||||
|
||||
return settings
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"Error getting settings for guild {guild_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error getting settings: {str(e)}"
|
||||
)
|
||||
|
||||
@router.patch("/guilds/{guild_id}/settings", status_code=status.HTTP_200_OK)
|
||||
async def update_guild_settings(
|
||||
guild_id: int,
|
||||
@ -444,6 +520,13 @@ async def update_guild_settings(
|
||||
log.debug(f"Update data received: {settings_update}")
|
||||
|
||||
success_flags = []
|
||||
|
||||
# Get bot instance for core cogs check
|
||||
try:
|
||||
from discordbot import discord_bot_sync_api
|
||||
bot = discord_bot_sync_api.bot_instance
|
||||
core_cogs_list = bot.core_cogs if bot and hasattr(bot, 'core_cogs') else {'SettingsCog', 'HelpCog'}
|
||||
except ImportError:
|
||||
core_cogs_list = {'SettingsCog', 'HelpCog'} # Core cogs that cannot be disabled
|
||||
|
||||
# Update prefix if provided
|
||||
@ -494,6 +577,14 @@ async def update_guild_settings(
|
||||
else:
|
||||
log.warning(f"Attempted to change status of core cog '{cog_name}' for guild {guild_id} - ignored.")
|
||||
|
||||
# Update commands if provided
|
||||
if 'commands' in settings_update and isinstance(settings_update['commands'], dict):
|
||||
for command_name, enabled_status in settings_update['commands'].items():
|
||||
success = await settings_manager.set_command_enabled(guild_id, command_name, enabled_status)
|
||||
success_flags.append(success)
|
||||
if not success:
|
||||
log.error(f"Failed to update status for command '{command_name}' for guild {guild_id}")
|
||||
|
||||
if all(s is True for s in success_flags): # Check if all operations returned True
|
||||
return {"message": "Settings updated successfully."}
|
||||
else:
|
||||
@ -628,7 +719,7 @@ async def get_global_settings(
|
||||
)
|
||||
|
||||
# Convert from UserSettings to GlobalSettings
|
||||
return GlobalSettings(
|
||||
global_settings = GlobalSettings(
|
||||
system_message=user_settings.get("system_message", ""),
|
||||
character=user_settings.get("character", ""),
|
||||
character_info=user_settings.get("character_info", ""),
|
||||
@ -637,6 +728,20 @@ async def get_global_settings(
|
||||
temperature=user_settings.get("temperature", 0.7),
|
||||
max_tokens=user_settings.get("max_tokens", 1000)
|
||||
)
|
||||
|
||||
# Add theme settings if available
|
||||
if "theme" in user_settings:
|
||||
theme_data = user_settings["theme"]
|
||||
global_settings.theme = ThemeSettings(
|
||||
theme_mode=theme_data.get("theme_mode", "light"),
|
||||
primary_color=theme_data.get("primary_color", "#5865F2"),
|
||||
secondary_color=theme_data.get("secondary_color", "#2D3748"),
|
||||
accent_color=theme_data.get("accent_color", "#7289DA"),
|
||||
font_family=theme_data.get("font_family", "Inter, sans-serif"),
|
||||
custom_css=theme_data.get("custom_css")
|
||||
)
|
||||
|
||||
return global_settings
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
@ -688,6 +793,18 @@ async def update_global_settings(
|
||||
custom_instructions=settings.custom_instructions
|
||||
)
|
||||
|
||||
# Add theme settings if provided
|
||||
if settings.theme:
|
||||
from discordbot.api_service.api_models import ThemeSettings as ApiThemeSettings
|
||||
user_settings.theme = ApiThemeSettings(
|
||||
theme_mode=settings.theme.theme_mode,
|
||||
primary_color=settings.theme.primary_color,
|
||||
secondary_color=settings.theme.secondary_color,
|
||||
accent_color=settings.theme.accent_color,
|
||||
font_family=settings.theme.font_family,
|
||||
custom_css=settings.theme.custom_css
|
||||
)
|
||||
|
||||
# Save user settings to the database
|
||||
updated_settings = db.save_user_settings(user_id, user_settings)
|
||||
if not updated_settings:
|
||||
@ -708,6 +825,200 @@ async def update_global_settings(
|
||||
detail=f"Error updating global settings: {str(e)}"
|
||||
)
|
||||
|
||||
# --- Cog and Command Management Endpoints ---
|
||||
|
||||
@router.get("/guilds/{guild_id}/cogs", response_model=List[CogInfo])
|
||||
async def get_guild_cogs(
|
||||
guild_id: int,
|
||||
_user: dict = Depends(get_dashboard_user),
|
||||
_admin: bool = Depends(verify_dashboard_guild_admin)
|
||||
):
|
||||
"""Get all cogs and their commands for a guild."""
|
||||
try:
|
||||
# Check if bot instance is available via discord_bot_sync_api
|
||||
try:
|
||||
from discordbot import discord_bot_sync_api
|
||||
bot = discord_bot_sync_api.bot_instance
|
||||
if not bot:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Bot instance not available"
|
||||
)
|
||||
except ImportError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Bot sync API not available"
|
||||
)
|
||||
|
||||
# Get all cogs from the bot
|
||||
cogs_list = []
|
||||
for cog_name, cog in bot.cogs.items():
|
||||
# Get enabled status from settings_manager
|
||||
is_enabled = await settings_manager.is_cog_enabled(guild_id, cog_name, default_enabled=True)
|
||||
|
||||
# Get commands for this cog
|
||||
commands_list = []
|
||||
for command in cog.get_commands():
|
||||
# Get command enabled status
|
||||
cmd_enabled = await settings_manager.is_command_enabled(guild_id, command.qualified_name, default_enabled=True)
|
||||
commands_list.append({
|
||||
"name": command.qualified_name,
|
||||
"description": command.help or "No description available",
|
||||
"enabled": cmd_enabled
|
||||
})
|
||||
|
||||
# Add slash commands if any
|
||||
app_commands = [cmd for cmd in bot.tree.get_commands() if hasattr(cmd, 'cog') and cmd.cog and cmd.cog.qualified_name == cog_name]
|
||||
for cmd in app_commands:
|
||||
# Get command enabled status
|
||||
cmd_enabled = await settings_manager.is_command_enabled(guild_id, cmd.name, default_enabled=True)
|
||||
if not any(c["name"] == cmd.name for c in commands_list): # Avoid duplicates
|
||||
commands_list.append({
|
||||
"name": cmd.name,
|
||||
"description": cmd.description or "No description available",
|
||||
"enabled": cmd_enabled
|
||||
})
|
||||
|
||||
cogs_list.append(CogInfo(
|
||||
name=cog_name,
|
||||
description=cog.__doc__ or "No description available",
|
||||
enabled=is_enabled,
|
||||
commands=commands_list
|
||||
))
|
||||
|
||||
return cogs_list
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"Error getting cogs for guild {guild_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error getting cogs: {str(e)}"
|
||||
)
|
||||
|
||||
@router.patch("/guilds/{guild_id}/cogs/{cog_name}", status_code=status.HTTP_200_OK)
|
||||
async def update_cog_status(
|
||||
guild_id: int,
|
||||
cog_name: str,
|
||||
enabled: bool = Body(..., embed=True),
|
||||
_user: dict = Depends(get_dashboard_user),
|
||||
_admin: bool = Depends(verify_dashboard_guild_admin)
|
||||
):
|
||||
"""Enable or disable a cog for a guild."""
|
||||
try:
|
||||
# Check if settings_manager is available
|
||||
if not settings_manager or not settings_manager.pg_pool:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Settings manager not available"
|
||||
)
|
||||
|
||||
# Check if the cog exists
|
||||
try:
|
||||
from discordbot import discord_bot_sync_api
|
||||
bot = discord_bot_sync_api.bot_instance
|
||||
if not bot:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Bot instance not available"
|
||||
)
|
||||
|
||||
if cog_name not in bot.cogs:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Cog '{cog_name}' not found"
|
||||
)
|
||||
|
||||
# Check if it's a core cog
|
||||
if cog_name in bot.core_cogs:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Core cog '{cog_name}' cannot be disabled"
|
||||
)
|
||||
except ImportError:
|
||||
# If we can't import the bot, we'll just assume the cog exists
|
||||
log.warning("Bot sync API not available, skipping cog existence check")
|
||||
|
||||
# Update the cog enabled status
|
||||
success = await settings_manager.set_cog_enabled(guild_id, cog_name, enabled)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update cog '{cog_name}' status"
|
||||
)
|
||||
|
||||
return {"message": f"Cog '{cog_name}' {'enabled' if enabled else 'disabled'} successfully"}
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"Error updating cog status for guild {guild_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error updating cog status: {str(e)}"
|
||||
)
|
||||
|
||||
@router.patch("/guilds/{guild_id}/commands/{command_name}", status_code=status.HTTP_200_OK)
|
||||
async def update_command_status(
|
||||
guild_id: int,
|
||||
command_name: str,
|
||||
enabled: bool = Body(..., embed=True),
|
||||
_user: dict = Depends(get_dashboard_user),
|
||||
_admin: bool = Depends(verify_dashboard_guild_admin)
|
||||
):
|
||||
"""Enable or disable a command for a guild."""
|
||||
try:
|
||||
# Check if settings_manager is available
|
||||
if not settings_manager or not settings_manager.pg_pool:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Settings manager not available"
|
||||
)
|
||||
|
||||
# Check if the command exists
|
||||
try:
|
||||
from discordbot import discord_bot_sync_api
|
||||
bot = discord_bot_sync_api.bot_instance
|
||||
if not bot:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Bot instance not available"
|
||||
)
|
||||
|
||||
# Check if it's a prefix command
|
||||
command = bot.get_command(command_name)
|
||||
if not command:
|
||||
# Check if it's an app command
|
||||
app_commands = [cmd for cmd in bot.tree.get_commands() if cmd.name == command_name]
|
||||
if not app_commands:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Command '{command_name}' not found"
|
||||
)
|
||||
except ImportError:
|
||||
# If we can't import the bot, we'll just assume the command exists
|
||||
log.warning("Bot sync API not available, skipping command existence check")
|
||||
|
||||
# Update the command enabled status
|
||||
success = await settings_manager.set_command_enabled(guild_id, command_name, enabled)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update command '{command_name}' status"
|
||||
)
|
||||
|
||||
return {"message": f"Command '{command_name}' {'enabled' if enabled else 'disabled'} successfully"}
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"Error updating command status for guild {guild_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error updating command status: {str(e)}"
|
||||
)
|
||||
|
||||
# --- Conversations Endpoints ---
|
||||
|
||||
@router.get("/conversations", response_model=List[Conversation])
|
||||
|
60
api_service/dashboard_web/cog-management.html
Normal file
60
api_service/dashboard_web/cog-management.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!-- Cog Management Section -->
|
||||
<div id="cog-management-section" class="dashboard-section" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Manage Cogs & Commands</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cog-guild-select">Select Server:</label>
|
||||
<select name="guilds" id="cog-guild-select" class="w-full">
|
||||
<option value="">--Please choose a server--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cog-management-loading" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading cogs and commands...</p>
|
||||
</div>
|
||||
|
||||
<div id="cog-management-content" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Cogs (Modules)</h3>
|
||||
<p class="text-sm text-muted">Enable or disable entire modules of functionality</p>
|
||||
</div>
|
||||
<div class="cogs-list-container p-4">
|
||||
<div id="cogs-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Cogs will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group mt-4">
|
||||
<button id="save-cogs-button" class="btn btn-primary">Save Cog Settings</button>
|
||||
</div>
|
||||
<p id="cogs-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<div class="card mt-6">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Commands</h3>
|
||||
<p class="text-sm text-muted">Enable or disable individual commands</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cog-filter">Filter by Cog:</label>
|
||||
<select id="cog-filter" class="w-full">
|
||||
<option value="all">All Cogs</option>
|
||||
<!-- Cog options will be populated here -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="commands-list-container p-4">
|
||||
<div id="commands-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Commands will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group mt-4">
|
||||
<button id="save-commands-button" class="btn btn-primary">Save Command Settings</button>
|
||||
</div>
|
||||
<p id="commands-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
161
api_service/dashboard_web/command-customization.html
Normal file
161
api_service/dashboard_web/command-customization.html
Normal file
@ -0,0 +1,161 @@
|
||||
<!-- Command Customization Section -->
|
||||
<div id="command-customization-section" class="dashboard-section" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Command Customization</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="command-customization-form">
|
||||
<!-- Command Customization Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Customize Commands</h3>
|
||||
<p class="text-muted">Customize the names and descriptions of commands for your server.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="search-container">
|
||||
<input type="text" id="command-search" placeholder="Search commands..." class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div id="command-list" class="command-list">
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Group Customization Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Customize Command Groups</h3>
|
||||
<p class="text-muted">Customize the names of command groups for your server.</p>
|
||||
</div>
|
||||
<div id="group-list" class="command-list">
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Aliases Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Command Aliases</h3>
|
||||
<p class="text-muted">Add alternative names for commands.</p>
|
||||
</div>
|
||||
<div id="alias-list" class="command-list">
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<h4>Add New Alias</h4>
|
||||
<div class="flex-row">
|
||||
<div class="flex-col mr-2">
|
||||
<label for="alias-command-select">Command:</label>
|
||||
<select id="alias-command-select" class="w-full">
|
||||
<option value="">Select a command</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<label for="alias-name-input">Alias:</label>
|
||||
<input type="text" id="alias-name-input" placeholder="Enter alias name" class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
<button id="add-alias-button" class="btn btn-primary mt-2">Add Alias</button>
|
||||
<p id="alias-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Commands Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Sync Commands</h3>
|
||||
<p class="text-muted">Sync command customizations to Discord.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p>After making changes to command names, descriptions, or aliases, you need to sync the changes to Discord.</p>
|
||||
<button id="sync-commands-button" class="btn btn-primary">Sync Commands</button>
|
||||
<p id="sync-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Customization Template -->
|
||||
<template id="command-item-template">
|
||||
<div class="command-item">
|
||||
<div class="command-header">
|
||||
<h4 class="command-name"></h4>
|
||||
<div class="command-actions">
|
||||
<button class="btn btn-sm btn-primary edit-command-btn">Edit</button>
|
||||
<button class="btn btn-sm btn-warning reset-command-btn">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-details">
|
||||
<p class="command-description"></p>
|
||||
<div class="command-customization" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>Custom Name:</label>
|
||||
<input type="text" class="custom-command-name w-full" placeholder="Enter custom name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Custom Description:</label>
|
||||
<input type="text" class="custom-command-description w-full" placeholder="Enter custom description">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary save-command-btn">Save</button>
|
||||
<button class="btn btn-sm btn-secondary cancel-command-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Group Customization Template -->
|
||||
<template id="group-item-template">
|
||||
<div class="command-item">
|
||||
<div class="command-header">
|
||||
<h4 class="group-name"></h4>
|
||||
<div class="command-actions">
|
||||
<button class="btn btn-sm btn-primary edit-group-btn">Edit</button>
|
||||
<button class="btn btn-sm btn-warning reset-group-btn">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-details">
|
||||
<div class="group-customization" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>Custom Name:</label>
|
||||
<input type="text" class="custom-group-name w-full" placeholder="Enter custom name">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary save-group-btn">Save</button>
|
||||
<button class="btn btn-sm btn-secondary cancel-group-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Alias Item Template -->
|
||||
<template id="alias-item-template">
|
||||
<div class="alias-item">
|
||||
<div class="alias-header">
|
||||
<h4 class="command-name"></h4>
|
||||
</div>
|
||||
<div class="alias-list">
|
||||
<ul class="alias-tags">
|
||||
<!-- Alias tags will be added here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Alias Tag Template -->
|
||||
<template id="alias-tag-template">
|
||||
<li class="alias-tag">
|
||||
<span class="alias-name"></span>
|
||||
<button class="remove-alias-btn">×</button>
|
||||
</li>
|
||||
</template>
|
115
api_service/dashboard_web/css/cog-management.css
Normal file
115
api_service/dashboard_web/css/cog-management.css
Normal file
@ -0,0 +1,115 @@
|
||||
/* Cog Management Styles */
|
||||
|
||||
.cog-card, .command-card {
|
||||
background-color: var(--card-bg);
|
||||
border-color: var(--border-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cog-card:hover, .command-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cog-badge {
|
||||
background-color: var(--primary-color-light);
|
||||
color: var(--primary-color-dark);
|
||||
}
|
||||
|
||||
.command-count {
|
||||
background-color: var(--secondary-color-light);
|
||||
color: var(--secondary-color-dark);
|
||||
}
|
||||
|
||||
.cogs-list-container, .commands-list-container {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Loading container */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top: 4px solid var(--primary-color);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--card-bg);
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-right: 0.5rem;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.3rem;
|
||||
top: 0.1rem;
|
||||
width: 0.5rem;
|
||||
height: 0.8rem;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled {
|
||||
background-color: var(--disabled-bg);
|
||||
border-color: var(--disabled-border);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled:checked {
|
||||
background-color: var(--disabled-checked-bg);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled:checked::after {
|
||||
border-color: var(--disabled-checked-color);
|
||||
}
|
||||
|
||||
/* Grid layout for larger screens */
|
||||
@media (min-width: 768px) {
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Feedback messages */
|
||||
.text-green-600 {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
162
api_service/dashboard_web/css/command-customization.css
Normal file
162
api_service/dashboard_web/css/command-customization.css
Normal file
@ -0,0 +1,162 @@
|
||||
/* Command Customization CSS */
|
||||
|
||||
.command-list {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.command-item {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.command-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background-color: var(--light-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.command-name {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.command-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.command-details, .group-details {
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.command-description {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--spacing-3);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.command-customization, .group-customization {
|
||||
margin-top: var(--spacing-3);
|
||||
padding-top: var(--spacing-3);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Alias styles */
|
||||
.alias-item {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.alias-header {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background-color: var(--light-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.alias-list {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
}
|
||||
|
||||
.alias-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.alias-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.alias-name {
|
||||
margin-right: var(--spacing-1);
|
||||
}
|
||||
|
||||
.remove-alias-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.remove-alias-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Search container */
|
||||
.search-container {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
#command-search {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
body.dark-mode .command-header,
|
||||
body.dark-mode .alias-header {
|
||||
background-color: var(--dark-bg);
|
||||
border-bottom-color: var(--border-color);
|
||||
}
|
||||
|
||||
body.dark-mode .command-item,
|
||||
body.dark-mode .alias-item {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
body.dark-mode .command-description {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
body.dark-mode .command-customization,
|
||||
body.dark-mode .group-customization {
|
||||
border-top-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* Custom mode styles */
|
||||
body.custom-mode .alias-tag {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
.command-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.command-actions {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-col.mr-2 {
|
||||
margin-right: 0;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
}
|
199
api_service/dashboard_web/css/theme-settings.css
Normal file
199
api_service/dashboard_web/css/theme-settings.css
Normal file
@ -0,0 +1,199 @@
|
||||
/* Theme Settings CSS */
|
||||
|
||||
/* Color Picker Container */
|
||||
.color-picker-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-text-input {
|
||||
width: 100px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Theme Preview */
|
||||
.theme-preview {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: var(--spacing-4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.preview-button {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: var(--spacing-4);
|
||||
background-color: var(--light-bg);
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background-color: white;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-card-header {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preview-card-body {
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.preview-form-control {
|
||||
height: 40px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.preview-button-primary {
|
||||
display: inline-block;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
border-radius: var(--radius-md);
|
||||
margin-right: var(--spacing-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-button-secondary {
|
||||
display: inline-block;
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Dark Mode Preview */
|
||||
.theme-preview.dark-mode {
|
||||
--preview-bg: #1A202C;
|
||||
--preview-card-bg: #2D3748;
|
||||
--preview-text: #F7FAFC;
|
||||
--preview-border: #4A5568;
|
||||
}
|
||||
|
||||
.theme-preview.dark-mode .preview-content {
|
||||
background-color: var(--preview-bg);
|
||||
}
|
||||
|
||||
.theme-preview.dark-mode .preview-card {
|
||||
background-color: var(--preview-card-bg);
|
||||
color: var(--preview-text);
|
||||
}
|
||||
|
||||
.theme-preview.dark-mode .preview-card-header {
|
||||
border-bottom-color: var(--preview-border);
|
||||
}
|
||||
|
||||
.theme-preview.dark-mode .preview-form-control {
|
||||
border-color: var(--preview-border);
|
||||
background-color: #4A5568;
|
||||
}
|
||||
|
||||
/* Custom Mode Preview */
|
||||
.theme-preview.custom-mode .preview-header {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.theme-preview.custom-mode .preview-button-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.theme-preview.custom-mode .preview-button-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Dark Mode for Dashboard */
|
||||
body.dark-mode {
|
||||
--light-bg: #1A202C;
|
||||
--dark-bg: #171923;
|
||||
--card-bg: #2D3748;
|
||||
--text-primary: #F7FAFC;
|
||||
--text-secondary: #CBD5E0;
|
||||
--border-color: #4A5568;
|
||||
}
|
||||
|
||||
body.dark-mode .card {
|
||||
background-color: var(--card-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
body.dark-mode input[type="text"],
|
||||
body.dark-mode input[type="number"],
|
||||
body.dark-mode input[type="password"],
|
||||
body.dark-mode input[type="email"],
|
||||
body.dark-mode input[type="search"],
|
||||
body.dark-mode select,
|
||||
body.dark-mode textarea {
|
||||
background-color: #4A5568;
|
||||
color: var(--text-primary);
|
||||
border-color: #718096;
|
||||
}
|
||||
|
||||
body.dark-mode .sidebar {
|
||||
background-color: #171923;
|
||||
}
|
||||
|
||||
body.dark-mode .header {
|
||||
background-color: #2D3748;
|
||||
border-bottom-color: #4A5568;
|
||||
}
|
||||
|
||||
/* Custom Mode for Dashboard */
|
||||
body.custom-mode {
|
||||
--primary-color: var(--primary-color);
|
||||
--secondary-color: var(--secondary-color);
|
||||
--accent-color: var(--accent-color);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
body.custom-mode .btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
body.custom-mode .btn-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
body.custom-mode .sidebar {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
body.custom-mode .nav-item.active {
|
||||
background-color: var(--primary-color);
|
||||
}
|
@ -12,6 +12,9 @@
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/theme-settings.css">
|
||||
<link rel="stylesheet" href="css/command-customization.css">
|
||||
<link rel="stylesheet" href="css/cog-management.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Auth Section -->
|
||||
@ -46,6 +49,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
|
||||
Modules
|
||||
</a>
|
||||
<a href="#cog-management" class="nav-item" data-section="cog-management-section" id="nav-cog-management">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
Cog Management
|
||||
</a>
|
||||
<a href="#permissions-settings" class="nav-item" data-section="permissions-settings-section">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
Permissions
|
||||
@ -54,6 +61,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
AI Settings
|
||||
</a>
|
||||
<a href="#theme-settings" class="nav-item" data-section="theme-settings-section">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
Theme Settings
|
||||
</a>
|
||||
<a href="#command-customization" class="nav-item" data-section="command-customization-section">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
|
||||
Command Customization
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<button id="logout-button" class="btn btn-danger w-full">
|
||||
@ -366,6 +381,340 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Theme Settings Section -->
|
||||
<div id="theme-settings-template" style="display: none;">
|
||||
<!-- This template will be used to restore the form after loading -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Theme Mode</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="theme-mode-light" name="theme_mode" value="light" checked>
|
||||
<label for="theme-mode-light">Light Mode</label>
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="theme-mode-dark" name="theme_mode" value="dark">
|
||||
<label for="theme-mode-dark">Dark Mode</label>
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="theme-mode-custom" name="theme_mode" value="custom">
|
||||
<label for="theme-mode-custom">Custom Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="custom-theme-settings" class="card" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Custom Colors</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="primary-color">Primary Color:</label>
|
||||
<div class="color-picker-container">
|
||||
<input type="color" id="primary-color" value="#5865F2">
|
||||
<input type="text" id="primary-color-text" value="#5865F2" class="color-text-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="secondary-color">Secondary Color:</label>
|
||||
<div class="color-picker-container">
|
||||
<input type="color" id="secondary-color" value="#2D3748">
|
||||
<input type="text" id="secondary-color-text" value="#2D3748" class="color-text-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="accent-color">Accent Color:</label>
|
||||
<div class="color-picker-container">
|
||||
<input type="color" id="accent-color" value="#7289DA">
|
||||
<input type="text" id="accent-color-text" value="#7289DA" class="color-text-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="font-family">Font Family:</label>
|
||||
<select id="font-family" class="w-full">
|
||||
<option value="Inter, sans-serif">Inter</option>
|
||||
<option value="'Roboto', sans-serif">Roboto</option>
|
||||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||||
<option value="'Montserrat', sans-serif">Montserrat</option>
|
||||
<option value="'Poppins', sans-serif">Poppins</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-css">Custom CSS (Advanced):</label>
|
||||
<textarea id="custom-css" rows="6" class="w-full" placeholder="Enter custom CSS here..."></textarea>
|
||||
<small class="text-muted">Custom CSS will be applied to the dashboard. Use with caution.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Theme Preview</h3>
|
||||
</div>
|
||||
<div id="theme-preview" class="theme-preview">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">Header</div>
|
||||
<div class="preview-button">Button</div>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div class="preview-card">
|
||||
<div class="preview-card-header">Card Title</div>
|
||||
<div class="preview-card-body">
|
||||
<p>This is a preview of how your theme will look.</p>
|
||||
<div class="preview-form-control"></div>
|
||||
<div class="preview-button-primary">Primary Button</div>
|
||||
<div class="preview-button-secondary">Secondary Button</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="btn-group">
|
||||
<button id="save-theme-settings-button" class="btn btn-primary">Save Theme Settings</button>
|
||||
<button id="reset-theme-settings-button" class="btn btn-warning">Reset to Defaults</button>
|
||||
</div>
|
||||
<p id="theme-settings-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Settings Section -->
|
||||
<div id="theme-settings-section" class="dashboard-section" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Theme Settings</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="theme-settings-form">
|
||||
<!-- Will be populated by JS -->
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Cog Management Section -->
|
||||
<!-- Cog Management Section -->
|
||||
<div id="cog-management-section" class="dashboard-section" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Manage Cogs & Commands</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cog-guild-select">Select Server:</label>
|
||||
<select name="guilds" id="cog-guild-select" class="w-full">
|
||||
<option value="">--Please choose a server--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cog-management-loading" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading cogs and commands...</p>
|
||||
</div>
|
||||
|
||||
<div id="cog-management-content" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Cogs (Modules)</h3>
|
||||
<p class="text-sm text-muted">Enable or disable entire modules of functionality</p>
|
||||
</div>
|
||||
<div class="cogs-list-container p-4">
|
||||
<div id="cogs-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Cogs will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group mt-4">
|
||||
<button id="save-cogs-button" class="btn btn-primary">Save Cog Settings</button>
|
||||
</div>
|
||||
<p id="cogs-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<div class="card mt-6">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Commands</h3>
|
||||
<p class="text-sm text-muted">Enable or disable individual commands</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cog-filter">Filter by Cog:</label>
|
||||
<select id="cog-filter" class="w-full">
|
||||
<option value="all">All Cogs</option>
|
||||
<!-- Cog options will be populated here -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="commands-list-container p-4">
|
||||
<div id="commands-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Commands will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group mt-4">
|
||||
<button id="save-commands-button" class="btn btn-primary">Save Command Settings</button>
|
||||
</div>
|
||||
<p id="commands-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Command Customization Section -->
|
||||
<div id="command-customization-section" class="dashboard-section" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Command Customization</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="command-customization-form">
|
||||
<!-- Command Customization Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Customize Commands</h3>
|
||||
<p class="text-muted">Customize the names and descriptions of commands for your server.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="search-container">
|
||||
<input type="text" id="command-search" placeholder="Search commands..." class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div id="command-list" class="command-list">
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Group Customization Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Customize Command Groups</h3>
|
||||
<p class="text-muted">Customize the names of command groups for your server.</p>
|
||||
</div>
|
||||
<div id="group-list" class="command-list">
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Aliases Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Command Aliases</h3>
|
||||
<p class="text-muted">Add alternative names for commands.</p>
|
||||
</div>
|
||||
<div id="alias-list" class="command-list">
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<h4>Add New Alias</h4>
|
||||
<div class="flex-row">
|
||||
<div class="flex-col mr-2">
|
||||
<label for="alias-command-select">Command:</label>
|
||||
<select id="alias-command-select" class="w-full">
|
||||
<option value="">Select a command</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<label for="alias-name-input">Alias:</label>
|
||||
<input type="text" id="alias-name-input" placeholder="Enter alias name" class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
<button id="add-alias-button" class="btn btn-primary mt-2">Add Alias</button>
|
||||
<p id="alias-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Commands Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Sync Commands</h3>
|
||||
<p class="text-muted">Sync command customizations to Discord.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p>After making changes to command names, descriptions, or aliases, you need to sync the changes to Discord.</p>
|
||||
<button id="sync-commands-button" class="btn btn-primary">Sync Commands</button>
|
||||
<p id="sync-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Customization Templates -->
|
||||
<template id="command-item-template">
|
||||
<div class="command-item">
|
||||
<div class="command-header">
|
||||
<h4 class="command-name"></h4>
|
||||
<div class="command-actions">
|
||||
<button class="btn btn-sm btn-primary edit-command-btn">Edit</button>
|
||||
<button class="btn btn-sm btn-warning reset-command-btn">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-details">
|
||||
<p class="command-description"></p>
|
||||
<div class="command-customization" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>Custom Name:</label>
|
||||
<input type="text" class="custom-command-name w-full" placeholder="Enter custom name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Custom Description:</label>
|
||||
<input type="text" class="custom-command-description w-full" placeholder="Enter custom description">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary save-command-btn">Save</button>
|
||||
<button class="btn btn-sm btn-secondary cancel-command-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="group-item-template">
|
||||
<div class="command-item">
|
||||
<div class="command-header">
|
||||
<h4 class="group-name"></h4>
|
||||
<div class="command-actions">
|
||||
<button class="btn btn-sm btn-primary edit-group-btn">Edit</button>
|
||||
<button class="btn btn-sm btn-warning reset-group-btn">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-details">
|
||||
<div class="group-customization" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>Custom Name:</label>
|
||||
<input type="text" class="custom-group-name w-full" placeholder="Enter custom name">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary save-group-btn">Save</button>
|
||||
<button class="btn btn-sm btn-secondary cancel-group-btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="alias-item-template">
|
||||
<div class="alias-item">
|
||||
<div class="alias-header">
|
||||
<h4 class="command-name"></h4>
|
||||
</div>
|
||||
<div class="alias-list">
|
||||
<ul class="alias-tags">
|
||||
<!-- Alias tags will be added here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="alias-tag-template">
|
||||
<li class="alias-tag">
|
||||
<span class="alias-name"></span>
|
||||
<button class="remove-alias-btn">×</button>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -376,5 +725,8 @@
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
<script src="js/ai-settings.js"></script>
|
||||
<script src="js/theme-settings.js"></script>
|
||||
<script src="js/command-customization.js"></script>
|
||||
<script src="js/cog-management.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
466
api_service/dashboard_web/js/cog-management.js
Normal file
466
api_service/dashboard_web/js/cog-management.js
Normal file
@ -0,0 +1,466 @@
|
||||
/**
|
||||
* Cog Management JavaScript
|
||||
* Handles cog and command enabling/disabling functionality
|
||||
*/
|
||||
|
||||
// Global variables
|
||||
let cogsData = [];
|
||||
let commandsData = {};
|
||||
let selectedGuildId = null;
|
||||
let cogManagementLoaded = false;
|
||||
|
||||
// Initialize cog management when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initCogManagement();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize cog management functionality
|
||||
*/
|
||||
function initCogManagement() {
|
||||
// Get DOM elements
|
||||
const cogGuildSelect = document.getElementById('cog-guild-select');
|
||||
const cogFilter = document.getElementById('cog-filter');
|
||||
const saveCogsButton = document.getElementById('save-cogs-button');
|
||||
const saveCommandsButton = document.getElementById('save-commands-button');
|
||||
const navCogManagement = document.getElementById('nav-cog-management');
|
||||
|
||||
// Add event listener for cog management tab
|
||||
if (navCogManagement) {
|
||||
navCogManagement.addEventListener('click', () => {
|
||||
// Show cog management section
|
||||
showSection('cog-management');
|
||||
|
||||
// Load guilds if not already loaded
|
||||
if (!cogManagementLoaded) {
|
||||
loadGuildsForCogManagement();
|
||||
cogManagementLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener for guild select
|
||||
if (cogGuildSelect) {
|
||||
cogGuildSelect.addEventListener('change', () => {
|
||||
selectedGuildId = cogGuildSelect.value;
|
||||
if (selectedGuildId) {
|
||||
loadCogsAndCommands(selectedGuildId);
|
||||
} else {
|
||||
// Hide content if no guild selected
|
||||
document.getElementById('cog-management-content').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener for cog filter
|
||||
if (cogFilter) {
|
||||
cogFilter.addEventListener('change', () => {
|
||||
filterCommands(cogFilter.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener for save cogs button
|
||||
if (saveCogsButton) {
|
||||
saveCogsButton.addEventListener('click', () => {
|
||||
saveCogsSettings();
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener for save commands button
|
||||
if (saveCommandsButton) {
|
||||
saveCommandsButton.addEventListener('click', () => {
|
||||
saveCommandsSettings();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load guilds for cog management
|
||||
*/
|
||||
function loadGuildsForCogManagement() {
|
||||
const cogGuildSelect = document.getElementById('cog-guild-select');
|
||||
|
||||
// Show loading state
|
||||
cogGuildSelect.disabled = true;
|
||||
cogGuildSelect.innerHTML = '<option value="">Loading servers...</option>';
|
||||
|
||||
// Fetch guilds from API
|
||||
API.get('/dashboard/api/guilds')
|
||||
.then(guilds => {
|
||||
// Clear loading state
|
||||
cogGuildSelect.innerHTML = '<option value="">--Please choose a server--</option>';
|
||||
|
||||
// Add guilds to select
|
||||
guilds.forEach(guild => {
|
||||
const option = document.createElement('option');
|
||||
option.value = guild.id;
|
||||
option.textContent = guild.name;
|
||||
cogGuildSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Enable select
|
||||
cogGuildSelect.disabled = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading guilds:', error);
|
||||
cogGuildSelect.innerHTML = '<option value="">Error loading servers</option>';
|
||||
cogGuildSelect.disabled = false;
|
||||
Toast.error('Failed to load servers. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cogs and commands for a guild
|
||||
* @param {string} guildId - The guild ID
|
||||
*/
|
||||
function loadCogsAndCommands(guildId) {
|
||||
// Show loading state
|
||||
document.getElementById('cog-management-loading').style.display = 'flex';
|
||||
document.getElementById('cog-management-content').style.display = 'none';
|
||||
|
||||
// Fetch cogs and commands from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/cogs`)
|
||||
.then(data => {
|
||||
// Store data
|
||||
cogsData = data;
|
||||
|
||||
// Populate cogs list
|
||||
populateCogsUI(data);
|
||||
|
||||
// Populate commands list
|
||||
populateCommandsUI(data);
|
||||
|
||||
// Hide loading state
|
||||
document.getElementById('cog-management-loading').style.display = 'none';
|
||||
document.getElementById('cog-management-content').style.display = 'block';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading cogs and commands:', error);
|
||||
document.getElementById('cog-management-loading').style.display = 'none';
|
||||
Toast.error('Failed to load cogs and commands. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate cogs UI
|
||||
* @param {Array} cogs - Array of cog objects
|
||||
*/
|
||||
function populateCogsUI(cogs) {
|
||||
const cogsList = document.getElementById('cogs-list');
|
||||
const cogFilter = document.getElementById('cog-filter');
|
||||
|
||||
// Clear previous content
|
||||
cogsList.innerHTML = '';
|
||||
|
||||
// Clear filter options except "All Cogs"
|
||||
cogFilter.innerHTML = '<option value="all">All Cogs</option>';
|
||||
|
||||
// Add cogs to list
|
||||
cogs.forEach(cog => {
|
||||
// Create cog card
|
||||
const cogCard = document.createElement('div');
|
||||
cogCard.className = 'cog-card p-4 border rounded';
|
||||
|
||||
// Create cog header
|
||||
const cogHeader = document.createElement('div');
|
||||
cogHeader.className = 'cog-header flex items-center justify-between mb-2';
|
||||
|
||||
// Create cog checkbox
|
||||
const cogCheckbox = document.createElement('div');
|
||||
cogCheckbox.className = 'cog-checkbox flex items-center';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.id = `cog-${cog.name}`;
|
||||
checkbox.className = 'mr-2';
|
||||
checkbox.checked = cog.enabled;
|
||||
checkbox.dataset.cogName = cog.name;
|
||||
|
||||
// Disable checkbox for core cogs
|
||||
if (cog.name === 'SettingsCog' || cog.name === 'HelpCog') {
|
||||
checkbox.disabled = true;
|
||||
checkbox.title = 'Core cogs cannot be disabled';
|
||||
}
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `cog-${cog.name}`;
|
||||
label.textContent = cog.name;
|
||||
label.className = 'font-medium';
|
||||
|
||||
cogCheckbox.appendChild(checkbox);
|
||||
cogCheckbox.appendChild(label);
|
||||
|
||||
// Create command count badge
|
||||
const commandCount = document.createElement('span');
|
||||
commandCount.className = 'command-count bg-gray-200 text-gray-800 px-2 py-1 rounded text-xs';
|
||||
commandCount.textContent = `${cog.commands.length} commands`;
|
||||
|
||||
cogHeader.appendChild(cogCheckbox);
|
||||
cogHeader.appendChild(commandCount);
|
||||
|
||||
// Create cog description
|
||||
const cogDescription = document.createElement('p');
|
||||
cogDescription.className = 'cog-description text-sm text-gray-600 mt-1';
|
||||
cogDescription.textContent = cog.description || 'No description available';
|
||||
|
||||
// Add elements to cog card
|
||||
cogCard.appendChild(cogHeader);
|
||||
cogCard.appendChild(cogDescription);
|
||||
|
||||
// Add cog card to list
|
||||
cogsList.appendChild(cogCard);
|
||||
|
||||
// Add cog to filter options
|
||||
const option = document.createElement('option');
|
||||
option.value = cog.name;
|
||||
option.textContent = cog.name;
|
||||
cogFilter.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate commands UI
|
||||
* @param {Array} cogs - Array of cog objects
|
||||
*/
|
||||
function populateCommandsUI(cogs) {
|
||||
const commandsList = document.getElementById('commands-list');
|
||||
|
||||
// Clear previous content
|
||||
commandsList.innerHTML = '';
|
||||
|
||||
// Create a flat list of all commands with their cog
|
||||
commandsData = {};
|
||||
|
||||
cogs.forEach(cog => {
|
||||
cog.commands.forEach(command => {
|
||||
// Store command data with cog name
|
||||
commandsData[command.name] = {
|
||||
...command,
|
||||
cog_name: cog.name
|
||||
};
|
||||
|
||||
// Create command card
|
||||
const commandCard = createCommandCard(command, cog.name);
|
||||
|
||||
// Add command card to list
|
||||
commandsList.appendChild(commandCard);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a command card element
|
||||
* @param {Object} command - Command object
|
||||
* @param {string} cogName - Name of the cog the command belongs to
|
||||
* @returns {HTMLElement} Command card element
|
||||
*/
|
||||
function createCommandCard(command, cogName) {
|
||||
// Create command card
|
||||
const commandCard = document.createElement('div');
|
||||
commandCard.className = 'command-card p-4 border rounded';
|
||||
commandCard.dataset.cogName = cogName;
|
||||
|
||||
// Create command header
|
||||
const commandHeader = document.createElement('div');
|
||||
commandHeader.className = 'command-header flex items-center justify-between mb-2';
|
||||
|
||||
// Create command checkbox
|
||||
const commandCheckbox = document.createElement('div');
|
||||
commandCheckbox.className = 'command-checkbox flex items-center';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.id = `command-${command.name}`;
|
||||
checkbox.className = 'mr-2';
|
||||
checkbox.checked = command.enabled;
|
||||
checkbox.dataset.commandName = command.name;
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `command-${command.name}`;
|
||||
label.textContent = command.name;
|
||||
label.className = 'font-medium';
|
||||
|
||||
commandCheckbox.appendChild(checkbox);
|
||||
commandCheckbox.appendChild(label);
|
||||
|
||||
// Create cog badge
|
||||
const cogBadge = document.createElement('span');
|
||||
cogBadge.className = 'cog-badge bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs';
|
||||
cogBadge.textContent = cogName;
|
||||
|
||||
commandHeader.appendChild(commandCheckbox);
|
||||
commandHeader.appendChild(cogBadge);
|
||||
|
||||
// Create command description
|
||||
const commandDescription = document.createElement('p');
|
||||
commandDescription.className = 'command-description text-sm text-gray-600 mt-1';
|
||||
commandDescription.textContent = command.description || 'No description available';
|
||||
|
||||
// Add elements to command card
|
||||
commandCard.appendChild(commandHeader);
|
||||
commandCard.appendChild(commandDescription);
|
||||
|
||||
return commandCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter commands by cog
|
||||
* @param {string} cogName - Name of the cog to filter by, or "all" for all cogs
|
||||
*/
|
||||
function filterCommands(cogName) {
|
||||
const commandCards = document.querySelectorAll('.command-card');
|
||||
|
||||
commandCards.forEach(card => {
|
||||
if (cogName === 'all' || card.dataset.cogName === cogName) {
|
||||
card.style.display = 'block';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cogs settings
|
||||
*/
|
||||
function saveCogsSettings() {
|
||||
if (!selectedGuildId) return;
|
||||
|
||||
// Show loading state
|
||||
const saveButton = document.getElementById('save-cogs-button');
|
||||
saveButton.disabled = true;
|
||||
saveButton.textContent = 'Saving...';
|
||||
|
||||
// Get cog settings
|
||||
const cogsPayload = {};
|
||||
const cogCheckboxes = document.querySelectorAll('#cogs-list input[type="checkbox"]');
|
||||
|
||||
cogCheckboxes.forEach(checkbox => {
|
||||
if (!checkbox.disabled) {
|
||||
cogsPayload[checkbox.dataset.cogName] = checkbox.checked;
|
||||
}
|
||||
});
|
||||
|
||||
// Send request to API
|
||||
API.patch(`/dashboard/api/guilds/${selectedGuildId}/settings`, {
|
||||
cogs: cogsPayload
|
||||
})
|
||||
.then(() => {
|
||||
// Reset button state
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = 'Save Cog Settings';
|
||||
|
||||
// Show success message
|
||||
document.getElementById('cogs-feedback').textContent = 'Cog settings saved successfully!';
|
||||
document.getElementById('cogs-feedback').className = 'mt-2 text-green-600';
|
||||
|
||||
// Clear message after 3 seconds
|
||||
setTimeout(() => {
|
||||
document.getElementById('cogs-feedback').textContent = '';
|
||||
document.getElementById('cogs-feedback').className = 'mt-2';
|
||||
}, 3000);
|
||||
|
||||
Toast.success('Cog settings saved successfully!');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving cog settings:', error);
|
||||
|
||||
// Reset button state
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = 'Save Cog Settings';
|
||||
|
||||
// Show error message
|
||||
document.getElementById('cogs-feedback').textContent = 'Error saving cog settings. Please try again.';
|
||||
document.getElementById('cogs-feedback').className = 'mt-2 text-red-600';
|
||||
|
||||
Toast.error('Failed to save cog settings. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save commands settings
|
||||
*/
|
||||
function saveCommandsSettings() {
|
||||
if (!selectedGuildId) return;
|
||||
|
||||
// Show loading state
|
||||
const saveButton = document.getElementById('save-commands-button');
|
||||
saveButton.disabled = true;
|
||||
saveButton.textContent = 'Saving...';
|
||||
|
||||
// Get command settings
|
||||
const commandsPayload = {};
|
||||
const commandCheckboxes = document.querySelectorAll('#commands-list input[type="checkbox"]');
|
||||
|
||||
commandCheckboxes.forEach(checkbox => {
|
||||
commandsPayload[checkbox.dataset.commandName] = checkbox.checked;
|
||||
});
|
||||
|
||||
// Send request to API
|
||||
API.patch(`/dashboard/api/guilds/${selectedGuildId}/settings`, {
|
||||
commands: commandsPayload
|
||||
})
|
||||
.then(() => {
|
||||
// Reset button state
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = 'Save Command Settings';
|
||||
|
||||
// Show success message
|
||||
document.getElementById('commands-feedback').textContent = 'Command settings saved successfully!';
|
||||
document.getElementById('commands-feedback').className = 'mt-2 text-green-600';
|
||||
|
||||
// Clear message after 3 seconds
|
||||
setTimeout(() => {
|
||||
document.getElementById('commands-feedback').textContent = '';
|
||||
document.getElementById('commands-feedback').className = 'mt-2';
|
||||
}, 3000);
|
||||
|
||||
Toast.success('Command settings saved successfully!');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving command settings:', error);
|
||||
|
||||
// Reset button state
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = 'Save Command Settings';
|
||||
|
||||
// Show error message
|
||||
document.getElementById('commands-feedback').textContent = 'Error saving command settings. Please try again.';
|
||||
document.getElementById('commands-feedback').className = 'mt-2 text-red-600';
|
||||
|
||||
Toast.error('Failed to save command settings. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a specific section and hide others
|
||||
* @param {string} sectionId - ID of the section to show
|
||||
*/
|
||||
function showSection(sectionId) {
|
||||
// Get all sections
|
||||
const sections = document.querySelectorAll('.dashboard-section');
|
||||
|
||||
// Hide all sections
|
||||
sections.forEach(section => {
|
||||
section.style.display = 'none';
|
||||
});
|
||||
|
||||
// Get all nav buttons
|
||||
const navButtons = document.querySelectorAll('.nav-button');
|
||||
|
||||
// Remove active class from all nav buttons
|
||||
navButtons.forEach(button => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show the selected section and activate the corresponding nav button
|
||||
const selectedSection = document.getElementById(`${sectionId}-section`);
|
||||
const selectedNavButton = document.getElementById(`nav-${sectionId}`);
|
||||
|
||||
if (selectedSection) {
|
||||
selectedSection.style.display = 'block';
|
||||
}
|
||||
|
||||
if (selectedNavButton) {
|
||||
selectedNavButton.classList.add('active');
|
||||
}
|
||||
}
|
781
api_service/dashboard_web/js/command-customization.js
Normal file
781
api_service/dashboard_web/js/command-customization.js
Normal file
@ -0,0 +1,781 @@
|
||||
/**
|
||||
* Command Customization JavaScript
|
||||
* Handles command customization functionality for the dashboard
|
||||
*/
|
||||
|
||||
// Initialize command customization when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initCommandCustomization();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize command customization
|
||||
*/
|
||||
function initCommandCustomization() {
|
||||
// Add event listener to command search input
|
||||
const commandSearch = document.getElementById('command-search');
|
||||
if (commandSearch) {
|
||||
commandSearch.addEventListener('input', filterCommands);
|
||||
}
|
||||
|
||||
// Add event listener to sync commands button
|
||||
const syncCommandsButton = document.getElementById('sync-commands-button');
|
||||
if (syncCommandsButton) {
|
||||
syncCommandsButton.addEventListener('click', syncCommands);
|
||||
}
|
||||
|
||||
// Add event listener to add alias button
|
||||
const addAliasButton = document.getElementById('add-alias-button');
|
||||
if (addAliasButton) {
|
||||
addAliasButton.addEventListener('click', addAlias);
|
||||
}
|
||||
|
||||
// Load command customizations when the section is shown
|
||||
const navItem = document.querySelector('a[data-section="command-customization-section"]');
|
||||
if (navItem) {
|
||||
navItem.addEventListener('click', () => {
|
||||
loadCommandCustomizations();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load command customizations from API
|
||||
*/
|
||||
async function loadCommandCustomizations() {
|
||||
try {
|
||||
// Show loading spinners
|
||||
document.getElementById('command-list').innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
|
||||
document.getElementById('group-list').innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
|
||||
document.getElementById('alias-list').innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
|
||||
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
showToast('error', 'Error', 'No guild selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch command customizations from API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load command customizations');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Render command customizations
|
||||
renderCommandCustomizations(data.command_customizations);
|
||||
|
||||
// Render group customizations
|
||||
renderGroupCustomizations(data.group_customizations);
|
||||
|
||||
// Render command aliases
|
||||
renderCommandAliases(data.command_aliases);
|
||||
|
||||
// Populate command select for aliases
|
||||
populateCommandSelect(Object.keys(data.command_customizations));
|
||||
} catch (error) {
|
||||
console.error('Error loading command customizations:', error);
|
||||
showToast('error', 'Error', 'Failed to load command customizations');
|
||||
|
||||
// Show error message in lists
|
||||
document.getElementById('command-list').innerHTML = '<div class="alert alert-danger">Failed to load command customizations</div>';
|
||||
document.getElementById('group-list').innerHTML = '<div class="alert alert-danger">Failed to load group customizations</div>';
|
||||
document.getElementById('alias-list').innerHTML = '<div class="alert alert-danger">Failed to load command aliases</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render command customizations
|
||||
* @param {Object} commandCustomizations - Command customizations object
|
||||
*/
|
||||
function renderCommandCustomizations(commandCustomizations) {
|
||||
const commandList = document.getElementById('command-list');
|
||||
commandList.innerHTML = '';
|
||||
|
||||
if (Object.keys(commandCustomizations).length === 0) {
|
||||
commandList.innerHTML = '<div class="alert alert-info">No commands found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort commands alphabetically
|
||||
const sortedCommands = Object.keys(commandCustomizations).sort();
|
||||
|
||||
// Create command items
|
||||
sortedCommands.forEach(commandName => {
|
||||
const customization = commandCustomizations[commandName];
|
||||
const commandItem = createCommandItem(commandName, customization);
|
||||
commandList.appendChild(commandItem);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a command item element
|
||||
* @param {string} commandName - Original command name
|
||||
* @param {Object} customization - Command customization object
|
||||
* @returns {HTMLElement} Command item element
|
||||
*/
|
||||
function createCommandItem(commandName, customization) {
|
||||
// Clone the template
|
||||
const template = document.getElementById('command-item-template');
|
||||
const commandItem = template.content.cloneNode(true).querySelector('.command-item');
|
||||
|
||||
// Set command name
|
||||
const nameElement = commandItem.querySelector('.command-name');
|
||||
nameElement.textContent = commandName;
|
||||
if (customization.name && customization.name !== commandName) {
|
||||
nameElement.textContent = `${customization.name} (${commandName})`;
|
||||
}
|
||||
|
||||
// Set command description
|
||||
const descriptionElement = commandItem.querySelector('.command-description');
|
||||
descriptionElement.textContent = customization.description || 'No description available';
|
||||
|
||||
// Set custom name input value
|
||||
const customNameInput = commandItem.querySelector('.custom-command-name');
|
||||
customNameInput.value = customization.name || '';
|
||||
customNameInput.placeholder = commandName;
|
||||
|
||||
// Set custom description input value
|
||||
const customDescriptionInput = commandItem.querySelector('.custom-command-description');
|
||||
customDescriptionInput.value = customization.description || '';
|
||||
|
||||
// Add event listeners to buttons
|
||||
const editButton = commandItem.querySelector('.edit-command-btn');
|
||||
const resetButton = commandItem.querySelector('.reset-command-btn');
|
||||
const saveButton = commandItem.querySelector('.save-command-btn');
|
||||
const cancelButton = commandItem.querySelector('.cancel-command-btn');
|
||||
const customizationDiv = commandItem.querySelector('.command-customization');
|
||||
|
||||
editButton.addEventListener('click', () => {
|
||||
customizationDiv.style.display = 'block';
|
||||
editButton.style.display = 'none';
|
||||
});
|
||||
|
||||
resetButton.addEventListener('click', () => {
|
||||
resetCommandCustomization(commandName);
|
||||
});
|
||||
|
||||
saveButton.addEventListener('click', () => {
|
||||
saveCommandCustomization(
|
||||
commandName,
|
||||
customNameInput.value,
|
||||
customDescriptionInput.value,
|
||||
customizationDiv,
|
||||
editButton,
|
||||
nameElement,
|
||||
descriptionElement
|
||||
);
|
||||
});
|
||||
|
||||
cancelButton.addEventListener('click', () => {
|
||||
customizationDiv.style.display = 'none';
|
||||
editButton.style.display = 'inline-block';
|
||||
|
||||
// Reset input values
|
||||
customNameInput.value = customization.name || '';
|
||||
customDescriptionInput.value = customization.description || '';
|
||||
});
|
||||
|
||||
// Add data attribute for filtering
|
||||
commandItem.dataset.commandName = commandName.toLowerCase();
|
||||
|
||||
return commandItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save command customization
|
||||
* @param {string} commandName - Original command name
|
||||
* @param {string} customName - Custom command name
|
||||
* @param {string} customDescription - Custom command description
|
||||
* @param {HTMLElement} customizationDiv - Command customization div
|
||||
* @param {HTMLElement} editButton - Edit button
|
||||
* @param {HTMLElement} nameElement - Command name element
|
||||
* @param {HTMLElement} descriptionElement - Command description element
|
||||
*/
|
||||
async function saveCommandCustomization(
|
||||
commandName,
|
||||
customName,
|
||||
customDescription,
|
||||
customizationDiv,
|
||||
editButton,
|
||||
nameElement,
|
||||
descriptionElement
|
||||
) {
|
||||
try {
|
||||
// Validate custom name format if provided
|
||||
if (customName && (!/^[a-z][a-z0-9_]*$/.test(customName) || customName.length > 32)) {
|
||||
showToast('error', 'Error', 'Custom command names must be lowercase, start with a letter, and contain only letters, numbers, and underscores (max 32 characters)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate custom description if provided
|
||||
if (customDescription && customDescription.length > 100) {
|
||||
showToast('error', 'Error', 'Custom command descriptions must be 100 characters or less');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
showToast('error', 'Error', 'No guild selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
command_name: commandName,
|
||||
custom_name: customName || null,
|
||||
custom_description: customDescription || null
|
||||
};
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}/commands`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save command customization');
|
||||
}
|
||||
|
||||
// Update UI
|
||||
customizationDiv.style.display = 'none';
|
||||
editButton.style.display = 'inline-block';
|
||||
|
||||
if (customName) {
|
||||
nameElement.textContent = `${customName} (${commandName})`;
|
||||
} else {
|
||||
nameElement.textContent = commandName;
|
||||
}
|
||||
|
||||
if (customDescription) {
|
||||
descriptionElement.textContent = customDescription;
|
||||
} else {
|
||||
descriptionElement.textContent = 'No description available';
|
||||
}
|
||||
|
||||
showToast('success', 'Success', 'Command customization saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Error saving command customization:', error);
|
||||
showToast('error', 'Error', 'Failed to save command customization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset command customization
|
||||
* @param {string} commandName - Original command name
|
||||
*/
|
||||
async function resetCommandCustomization(commandName) {
|
||||
try {
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
showToast('error', 'Error', 'No guild selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
command_name: commandName,
|
||||
custom_name: null,
|
||||
custom_description: null
|
||||
};
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}/commands`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to reset command customization');
|
||||
}
|
||||
|
||||
// Reload command customizations
|
||||
loadCommandCustomizations();
|
||||
|
||||
showToast('success', 'Success', 'Command customization reset successfully');
|
||||
} catch (error) {
|
||||
console.error('Error resetting command customization:', error);
|
||||
showToast('error', 'Error', 'Failed to reset command customization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render group customizations
|
||||
* @param {Object} groupCustomizations - Group customizations object
|
||||
*/
|
||||
function renderGroupCustomizations(groupCustomizations) {
|
||||
const groupList = document.getElementById('group-list');
|
||||
groupList.innerHTML = '';
|
||||
|
||||
if (Object.keys(groupCustomizations).length === 0) {
|
||||
groupList.innerHTML = '<div class="alert alert-info">No command groups found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort groups alphabetically
|
||||
const sortedGroups = Object.keys(groupCustomizations).sort();
|
||||
|
||||
// Create group items
|
||||
sortedGroups.forEach(groupName => {
|
||||
const customName = groupCustomizations[groupName];
|
||||
const groupItem = createGroupItem(groupName, customName);
|
||||
groupList.appendChild(groupItem);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group item element
|
||||
* @param {string} groupName - Original group name
|
||||
* @param {string} customName - Custom group name
|
||||
* @returns {HTMLElement} Group item element
|
||||
*/
|
||||
function createGroupItem(groupName, customName) {
|
||||
// Clone the template
|
||||
const template = document.getElementById('group-item-template');
|
||||
const groupItem = template.content.cloneNode(true).querySelector('.command-item');
|
||||
|
||||
// Set group name
|
||||
const nameElement = groupItem.querySelector('.group-name');
|
||||
nameElement.textContent = groupName;
|
||||
if (customName && customName !== groupName) {
|
||||
nameElement.textContent = `${customName} (${groupName})`;
|
||||
}
|
||||
|
||||
// Set custom name input value
|
||||
const customNameInput = groupItem.querySelector('.custom-group-name');
|
||||
customNameInput.value = customName || '';
|
||||
customNameInput.placeholder = groupName;
|
||||
|
||||
// Add event listeners to buttons
|
||||
const editButton = groupItem.querySelector('.edit-group-btn');
|
||||
const resetButton = groupItem.querySelector('.reset-group-btn');
|
||||
const saveButton = groupItem.querySelector('.save-group-btn');
|
||||
const cancelButton = groupItem.querySelector('.cancel-group-btn');
|
||||
const customizationDiv = groupItem.querySelector('.group-customization');
|
||||
|
||||
editButton.addEventListener('click', () => {
|
||||
customizationDiv.style.display = 'block';
|
||||
editButton.style.display = 'none';
|
||||
});
|
||||
|
||||
resetButton.addEventListener('click', () => {
|
||||
resetGroupCustomization(groupName);
|
||||
});
|
||||
|
||||
saveButton.addEventListener('click', () => {
|
||||
saveGroupCustomization(
|
||||
groupName,
|
||||
customNameInput.value,
|
||||
customizationDiv,
|
||||
editButton,
|
||||
nameElement
|
||||
);
|
||||
});
|
||||
|
||||
cancelButton.addEventListener('click', () => {
|
||||
customizationDiv.style.display = 'none';
|
||||
editButton.style.display = 'inline-block';
|
||||
|
||||
// Reset input value
|
||||
customNameInput.value = customName || '';
|
||||
});
|
||||
|
||||
return groupItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save group customization
|
||||
* @param {string} groupName - Original group name
|
||||
* @param {string} customName - Custom group name
|
||||
* @param {HTMLElement} customizationDiv - Group customization div
|
||||
* @param {HTMLElement} editButton - Edit button
|
||||
* @param {HTMLElement} nameElement - Group name element
|
||||
*/
|
||||
async function saveGroupCustomization(
|
||||
groupName,
|
||||
customName,
|
||||
customizationDiv,
|
||||
editButton,
|
||||
nameElement
|
||||
) {
|
||||
try {
|
||||
// Validate custom name format if provided
|
||||
if (customName && (!/^[a-z][a-z0-9_]*$/.test(customName) || customName.length > 32)) {
|
||||
showToast('error', 'Error', 'Custom group names must be lowercase, start with a letter, and contain only letters, numbers, and underscores (max 32 characters)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
showToast('error', 'Error', 'No guild selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
group_name: groupName,
|
||||
custom_name: customName || null
|
||||
};
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}/groups`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save group customization');
|
||||
}
|
||||
|
||||
// Update UI
|
||||
customizationDiv.style.display = 'none';
|
||||
editButton.style.display = 'inline-block';
|
||||
|
||||
if (customName) {
|
||||
nameElement.textContent = `${customName} (${groupName})`;
|
||||
} else {
|
||||
nameElement.textContent = groupName;
|
||||
}
|
||||
|
||||
showToast('success', 'Success', 'Group customization saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Error saving group customization:', error);
|
||||
showToast('error', 'Error', 'Failed to save group customization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset group customization
|
||||
* @param {string} groupName - Original group name
|
||||
*/
|
||||
async function resetGroupCustomization(groupName) {
|
||||
try {
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
showToast('error', 'Error', 'No guild selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
group_name: groupName,
|
||||
custom_name: null
|
||||
};
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}/groups`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to reset group customization');
|
||||
}
|
||||
|
||||
// Reload command customizations
|
||||
loadCommandCustomizations();
|
||||
|
||||
showToast('success', 'Success', 'Group customization reset successfully');
|
||||
} catch (error) {
|
||||
console.error('Error resetting group customization:', error);
|
||||
showToast('error', 'Error', 'Failed to reset group customization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render command aliases
|
||||
* @param {Object} commandAliases - Command aliases object
|
||||
*/
|
||||
function renderCommandAliases(commandAliases) {
|
||||
const aliasList = document.getElementById('alias-list');
|
||||
aliasList.innerHTML = '';
|
||||
|
||||
if (Object.keys(commandAliases).length === 0) {
|
||||
aliasList.innerHTML = '<div class="alert alert-info">No command aliases found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort commands alphabetically
|
||||
const sortedCommands = Object.keys(commandAliases).sort();
|
||||
|
||||
// Create alias items
|
||||
sortedCommands.forEach(commandName => {
|
||||
const aliases = commandAliases[commandName];
|
||||
if (aliases && aliases.length > 0) {
|
||||
const aliasItem = createAliasItem(commandName, aliases);
|
||||
aliasList.appendChild(aliasItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alias item element
|
||||
* @param {string} commandName - Original command name
|
||||
* @param {Array} aliases - Command aliases
|
||||
* @returns {HTMLElement} Alias item element
|
||||
*/
|
||||
function createAliasItem(commandName, aliases) {
|
||||
// Clone the template
|
||||
const template = document.getElementById('alias-item-template');
|
||||
const aliasItem = template.content.cloneNode(true).querySelector('.alias-item');
|
||||
|
||||
// Set command name
|
||||
const nameElement = aliasItem.querySelector('.command-name');
|
||||
nameElement.textContent = commandName;
|
||||
|
||||
// Add alias tags
|
||||
const aliasTagsList = aliasItem.querySelector('.alias-tags');
|
||||
aliases.forEach(alias => {
|
||||
const aliasTag = createAliasTag(commandName, alias);
|
||||
aliasTagsList.appendChild(aliasTag);
|
||||
});
|
||||
|
||||
return aliasItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alias tag element
|
||||
* @param {string} commandName - Original command name
|
||||
* @param {string} alias - Command alias
|
||||
* @returns {HTMLElement} Alias tag element
|
||||
*/
|
||||
function createAliasTag(commandName, alias) {
|
||||
// Clone the template
|
||||
const template = document.getElementById('alias-tag-template');
|
||||
const aliasTag = template.content.cloneNode(true).querySelector('.alias-tag');
|
||||
|
||||
// Set alias name
|
||||
const nameElement = aliasTag.querySelector('.alias-name');
|
||||
nameElement.textContent = alias;
|
||||
|
||||
// Add event listener to remove button
|
||||
const removeButton = aliasTag.querySelector('.remove-alias-btn');
|
||||
removeButton.addEventListener('click', () => {
|
||||
removeAlias(commandName, alias);
|
||||
});
|
||||
|
||||
return aliasTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate command select for aliases
|
||||
* @param {Array} commands - Command names
|
||||
*/
|
||||
function populateCommandSelect(commands) {
|
||||
const commandSelect = document.getElementById('alias-command-select');
|
||||
commandSelect.innerHTML = '<option value="">Select a command</option>';
|
||||
|
||||
// Sort commands alphabetically
|
||||
const sortedCommands = commands.sort();
|
||||
|
||||
// Add command options
|
||||
sortedCommands.forEach(commandName => {
|
||||
const option = document.createElement('option');
|
||||
option.value = commandName;
|
||||
option.textContent = commandName;
|
||||
commandSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a command alias
|
||||
*/
|
||||
async function addAlias() {
|
||||
try {
|
||||
// Get input values
|
||||
const commandSelect = document.getElementById('alias-command-select');
|
||||
const aliasInput = document.getElementById('alias-name-input');
|
||||
const feedbackElement = document.getElementById('alias-feedback');
|
||||
|
||||
const commandName = commandSelect.value;
|
||||
const aliasName = aliasInput.value.trim();
|
||||
|
||||
// Validate inputs
|
||||
if (!commandName) {
|
||||
feedbackElement.textContent = 'Please select a command';
|
||||
feedbackElement.className = 'mt-2 text-danger';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!aliasName) {
|
||||
feedbackElement.textContent = 'Please enter an alias name';
|
||||
feedbackElement.className = 'mt-2 text-danger';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate alias format
|
||||
if (!/^[a-z][a-z0-9_]*$/.test(aliasName) || aliasName.length > 32) {
|
||||
feedbackElement.textContent = 'Alias names must be lowercase, start with a letter, and contain only letters, numbers, and underscores (max 32 characters)';
|
||||
feedbackElement.className = 'mt-2 text-danger';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
feedbackElement.textContent = 'No guild selected';
|
||||
feedbackElement.className = 'mt-2 text-danger';
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
command_name: commandName,
|
||||
alias_name: aliasName
|
||||
};
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}/aliases`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add command alias');
|
||||
}
|
||||
|
||||
// Clear input
|
||||
aliasInput.value = '';
|
||||
|
||||
// Show success message
|
||||
feedbackElement.textContent = 'Alias added successfully';
|
||||
feedbackElement.className = 'mt-2 text-success';
|
||||
|
||||
// Reload command customizations
|
||||
loadCommandCustomizations();
|
||||
|
||||
showToast('success', 'Success', 'Command alias added successfully');
|
||||
} catch (error) {
|
||||
console.error('Error adding command alias:', error);
|
||||
|
||||
const feedbackElement = document.getElementById('alias-feedback');
|
||||
feedbackElement.textContent = 'Failed to add command alias';
|
||||
feedbackElement.className = 'mt-2 text-danger';
|
||||
|
||||
showToast('error', 'Error', 'Failed to add command alias');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a command alias
|
||||
* @param {string} commandName - Original command name
|
||||
* @param {string} aliasName - Command alias
|
||||
*/
|
||||
async function removeAlias(commandName, aliasName) {
|
||||
try {
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
showToast('error', 'Error', 'No guild selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
command_name: commandName,
|
||||
alias_name: aliasName
|
||||
};
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}/aliases`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to remove command alias');
|
||||
}
|
||||
|
||||
// Reload command customizations
|
||||
loadCommandCustomizations();
|
||||
|
||||
showToast('success', 'Success', 'Command alias removed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error removing command alias:', error);
|
||||
showToast('error', 'Error', 'Failed to remove command alias');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync commands to Discord
|
||||
*/
|
||||
async function syncCommands() {
|
||||
try {
|
||||
// Get the current guild ID
|
||||
const guildId = getCurrentGuildId();
|
||||
if (!guildId) {
|
||||
showToast('error', 'Error', 'No guild selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show feedback
|
||||
const feedbackElement = document.getElementById('sync-feedback');
|
||||
feedbackElement.textContent = 'Syncing commands...';
|
||||
feedbackElement.className = 'mt-2 text-info';
|
||||
|
||||
// Send request to API
|
||||
const response = await fetch(`/dashboard/commands/customizations/${guildId}/sync`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to sync commands');
|
||||
}
|
||||
|
||||
// Show success message
|
||||
feedbackElement.textContent = 'Commands synced successfully';
|
||||
feedbackElement.className = 'mt-2 text-success';
|
||||
|
||||
showToast('success', 'Success', 'Commands synced successfully');
|
||||
} catch (error) {
|
||||
console.error('Error syncing commands:', error);
|
||||
|
||||
// Show error message
|
||||
const feedbackElement = document.getElementById('sync-feedback');
|
||||
feedbackElement.textContent = 'Failed to sync commands';
|
||||
feedbackElement.className = 'mt-2 text-danger';
|
||||
|
||||
showToast('error', 'Error', 'Failed to sync commands');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter commands by search query
|
||||
*/
|
||||
function filterCommands() {
|
||||
const searchQuery = document.getElementById('command-search').value.toLowerCase();
|
||||
const commandItems = document.querySelectorAll('#command-list .command-item');
|
||||
|
||||
commandItems.forEach(item => {
|
||||
const commandName = item.dataset.commandName;
|
||||
if (commandName.includes(searchQuery)) {
|
||||
item.style.display = 'block';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
@ -312,6 +312,12 @@ function showSection(sectionId) {
|
||||
if (sectionId === 'ai-settings' && typeof loadAiSettings === 'function' && typeof aiSettingsLoaded !== 'undefined' && !aiSettingsLoaded) {
|
||||
loadAiSettings();
|
||||
}
|
||||
|
||||
// Load cog management if needed
|
||||
if (sectionId === 'cog-management' && typeof loadGuildsForCogManagement === 'function' && typeof cogManagementLoaded !== 'undefined' && !cogManagementLoaded) {
|
||||
loadGuildsForCogManagement();
|
||||
cogManagementLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
325
api_service/dashboard_web/js/theme-settings.js
Normal file
325
api_service/dashboard_web/js/theme-settings.js
Normal file
@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Theme Settings JavaScript
|
||||
* Handles theme customization for the dashboard
|
||||
*/
|
||||
|
||||
// Initialize theme settings when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initThemeSettings();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize theme settings
|
||||
*/
|
||||
function initThemeSettings() {
|
||||
// Get theme mode radio buttons
|
||||
const themeModeRadios = document.querySelectorAll('input[name="theme_mode"]');
|
||||
|
||||
// Add event listeners to theme mode radio buttons
|
||||
themeModeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
const customThemeSettings = document.getElementById('custom-theme-settings');
|
||||
if (radio.value === 'custom') {
|
||||
customThemeSettings.style.display = 'block';
|
||||
} else {
|
||||
customThemeSettings.style.display = 'none';
|
||||
}
|
||||
updateThemePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners to color pickers
|
||||
const colorInputs = document.querySelectorAll('input[type="color"]');
|
||||
colorInputs.forEach(input => {
|
||||
// Get the corresponding text input
|
||||
const textInput = document.getElementById(`${input.id}-text`);
|
||||
|
||||
// Update text input when color input changes
|
||||
input.addEventListener('input', () => {
|
||||
textInput.value = input.value;
|
||||
updateThemePreview();
|
||||
});
|
||||
|
||||
// Update color input when text input changes
|
||||
textInput.addEventListener('input', () => {
|
||||
// Validate hex color format
|
||||
if (/^#[0-9A-F]{6}$/i.test(textInput.value)) {
|
||||
input.value = textInput.value;
|
||||
updateThemePreview();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listener to font family select
|
||||
const fontFamilySelect = document.getElementById('font-family');
|
||||
fontFamilySelect.addEventListener('change', updateThemePreview);
|
||||
|
||||
// Add event listener to custom CSS textarea
|
||||
const customCssTextarea = document.getElementById('custom-css');
|
||||
customCssTextarea.addEventListener('input', updateThemePreview);
|
||||
|
||||
// Add event listener to save button
|
||||
const saveButton = document.getElementById('save-theme-settings-button');
|
||||
saveButton.addEventListener('click', saveThemeSettings);
|
||||
|
||||
// Add event listener to reset button
|
||||
const resetButton = document.getElementById('reset-theme-settings-button');
|
||||
resetButton.addEventListener('click', resetThemeSettings);
|
||||
|
||||
// Load theme settings from API
|
||||
loadThemeSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load theme settings from API
|
||||
*/
|
||||
async function loadThemeSettings() {
|
||||
try {
|
||||
// Show loading spinner
|
||||
const themeSettingsForm = document.getElementById('theme-settings-form');
|
||||
themeSettingsForm.innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
|
||||
|
||||
// Fetch theme settings from API
|
||||
const response = await fetch('/dashboard/api/settings');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load theme settings');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Restore the form
|
||||
themeSettingsForm.innerHTML = document.getElementById('theme-settings-template').innerHTML;
|
||||
|
||||
// Initialize event listeners again
|
||||
initThemeSettings();
|
||||
|
||||
// Set theme settings values
|
||||
if (data && data.theme) {
|
||||
const theme = data.theme;
|
||||
|
||||
// Set theme mode
|
||||
const themeModeRadio = document.querySelector(`input[name="theme_mode"][value="${theme.theme_mode}"]`);
|
||||
if (themeModeRadio) {
|
||||
themeModeRadio.checked = true;
|
||||
|
||||
// Show/hide custom theme settings
|
||||
const customThemeSettings = document.getElementById('custom-theme-settings');
|
||||
if (theme.theme_mode === 'custom') {
|
||||
customThemeSettings.style.display = 'block';
|
||||
} else {
|
||||
customThemeSettings.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Set color values
|
||||
if (theme.primary_color) {
|
||||
document.getElementById('primary-color').value = theme.primary_color;
|
||||
document.getElementById('primary-color-text').value = theme.primary_color;
|
||||
}
|
||||
|
||||
if (theme.secondary_color) {
|
||||
document.getElementById('secondary-color').value = theme.secondary_color;
|
||||
document.getElementById('secondary-color-text').value = theme.secondary_color;
|
||||
}
|
||||
|
||||
if (theme.accent_color) {
|
||||
document.getElementById('accent-color').value = theme.accent_color;
|
||||
document.getElementById('accent-color-text').value = theme.accent_color;
|
||||
}
|
||||
|
||||
// Set font family
|
||||
if (theme.font_family) {
|
||||
document.getElementById('font-family').value = theme.font_family;
|
||||
}
|
||||
|
||||
// Set custom CSS
|
||||
if (theme.custom_css) {
|
||||
document.getElementById('custom-css').value = theme.custom_css;
|
||||
}
|
||||
|
||||
// Update preview
|
||||
updateThemePreview();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading theme settings:', error);
|
||||
showToast('error', 'Error', 'Failed to load theme settings');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update theme preview
|
||||
*/
|
||||
function updateThemePreview() {
|
||||
const themeMode = document.querySelector('input[name="theme_mode"]:checked').value;
|
||||
const primaryColor = document.getElementById('primary-color').value;
|
||||
const secondaryColor = document.getElementById('secondary-color').value;
|
||||
const accentColor = document.getElementById('accent-color').value;
|
||||
const fontFamily = document.getElementById('font-family').value;
|
||||
const customCss = document.getElementById('custom-css').value;
|
||||
|
||||
const preview = document.getElementById('theme-preview');
|
||||
|
||||
// Apply theme mode
|
||||
if (themeMode === 'dark') {
|
||||
preview.classList.add('dark-mode');
|
||||
preview.classList.remove('custom-mode');
|
||||
} else if (themeMode === 'light') {
|
||||
preview.classList.remove('dark-mode');
|
||||
preview.classList.remove('custom-mode');
|
||||
} else if (themeMode === 'custom') {
|
||||
preview.classList.remove('dark-mode');
|
||||
preview.classList.add('custom-mode');
|
||||
|
||||
// Apply custom colors
|
||||
preview.style.setProperty('--primary-color', primaryColor);
|
||||
preview.style.setProperty('--secondary-color', secondaryColor);
|
||||
preview.style.setProperty('--accent-color', accentColor);
|
||||
preview.style.setProperty('--font-family', fontFamily);
|
||||
|
||||
// Apply custom CSS
|
||||
const customStyleElement = document.getElementById('custom-theme-style');
|
||||
if (customStyleElement) {
|
||||
customStyleElement.textContent = customCss;
|
||||
} else {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'custom-theme-style';
|
||||
style.textContent = customCss;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save theme settings
|
||||
*/
|
||||
async function saveThemeSettings() {
|
||||
try {
|
||||
const saveButton = document.getElementById('save-theme-settings-button');
|
||||
saveButton.classList.add('btn-loading');
|
||||
|
||||
const themeMode = document.querySelector('input[name="theme_mode"]:checked').value;
|
||||
const primaryColor = document.getElementById('primary-color').value;
|
||||
const secondaryColor = document.getElementById('secondary-color').value;
|
||||
const accentColor = document.getElementById('accent-color').value;
|
||||
const fontFamily = document.getElementById('font-family').value;
|
||||
const customCss = document.getElementById('custom-css').value;
|
||||
|
||||
// Create theme settings object
|
||||
const themeSettings = {
|
||||
theme_mode: themeMode,
|
||||
primary_color: primaryColor,
|
||||
secondary_color: secondaryColor,
|
||||
accent_color: accentColor,
|
||||
font_family: fontFamily,
|
||||
custom_css: customCss
|
||||
};
|
||||
|
||||
// Send theme settings to API
|
||||
const response = await fetch('/dashboard/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settings: {
|
||||
theme: themeSettings
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save theme settings');
|
||||
}
|
||||
|
||||
// Show success message
|
||||
const feedbackElement = document.getElementById('theme-settings-feedback');
|
||||
feedbackElement.textContent = 'Theme settings saved successfully!';
|
||||
feedbackElement.classList.add('text-success');
|
||||
|
||||
// Apply theme to the entire dashboard
|
||||
applyThemeToDocument(themeSettings);
|
||||
|
||||
// Show toast notification
|
||||
showToast('success', 'Success', 'Theme settings saved successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error saving theme settings:', error);
|
||||
|
||||
// Show error message
|
||||
const feedbackElement = document.getElementById('theme-settings-feedback');
|
||||
feedbackElement.textContent = 'Failed to save theme settings. Please try again.';
|
||||
feedbackElement.classList.add('text-danger');
|
||||
|
||||
// Show toast notification
|
||||
showToast('error', 'Error', 'Failed to save theme settings');
|
||||
} finally {
|
||||
// Remove loading state from button
|
||||
const saveButton = document.getElementById('save-theme-settings-button');
|
||||
saveButton.classList.remove('btn-loading');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset theme settings to defaults
|
||||
*/
|
||||
function resetThemeSettings() {
|
||||
// Set theme mode to light
|
||||
document.getElementById('theme-mode-light').checked = true;
|
||||
|
||||
// Hide custom theme settings
|
||||
document.getElementById('custom-theme-settings').style.display = 'none';
|
||||
|
||||
// Reset color values
|
||||
document.getElementById('primary-color').value = '#5865F2';
|
||||
document.getElementById('primary-color-text').value = '#5865F2';
|
||||
document.getElementById('secondary-color').value = '#2D3748';
|
||||
document.getElementById('secondary-color-text').value = '#2D3748';
|
||||
document.getElementById('accent-color').value = '#7289DA';
|
||||
document.getElementById('accent-color-text').value = '#7289DA';
|
||||
|
||||
// Reset font family
|
||||
document.getElementById('font-family').value = 'Inter, sans-serif';
|
||||
|
||||
// Reset custom CSS
|
||||
document.getElementById('custom-css').value = '';
|
||||
|
||||
// Update preview
|
||||
updateThemePreview();
|
||||
|
||||
// Show toast notification
|
||||
showToast('info', 'Reset', 'Theme settings reset to defaults');
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme to the entire document
|
||||
* @param {Object} theme - Theme settings object
|
||||
*/
|
||||
function applyThemeToDocument(theme) {
|
||||
// Apply theme mode
|
||||
if (theme.theme_mode === 'dark') {
|
||||
document.body.classList.add('dark-mode');
|
||||
document.body.classList.remove('custom-mode');
|
||||
} else if (theme.theme_mode === 'light') {
|
||||
document.body.classList.remove('dark-mode');
|
||||
document.body.classList.remove('custom-mode');
|
||||
} else if (theme.theme_mode === 'custom') {
|
||||
document.body.classList.remove('dark-mode');
|
||||
document.body.classList.add('custom-mode');
|
||||
|
||||
// Apply custom colors
|
||||
document.documentElement.style.setProperty('--primary-color', theme.primary_color);
|
||||
document.documentElement.style.setProperty('--secondary-color', theme.secondary_color);
|
||||
document.documentElement.style.setProperty('--accent-color', theme.accent_color);
|
||||
document.documentElement.style.setProperty('--font-family', theme.font_family);
|
||||
|
||||
// Apply custom CSS
|
||||
const customStyleElement = document.getElementById('global-custom-theme-style');
|
||||
if (customStyleElement) {
|
||||
customStyleElement.textContent = theme.custom_css;
|
||||
} else {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'global-custom-theme-style';
|
||||
style.textContent = theme.custom_css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}
|
107
api_service/dashboard_web/theme-settings.html
Normal file
107
api_service/dashboard_web/theme-settings.html
Normal file
@ -0,0 +1,107 @@
|
||||
<!-- Theme Settings Section -->
|
||||
<div id="theme-settings-section" class="dashboard-section" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Theme Settings</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="theme-settings-form">
|
||||
<!-- Theme Mode Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Theme Mode</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="theme-mode-light" name="theme_mode" value="light" checked>
|
||||
<label for="theme-mode-light">Light Mode</label>
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="theme-mode-dark" name="theme_mode" value="dark">
|
||||
<label for="theme-mode-dark">Dark Mode</label>
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="theme-mode-custom" name="theme_mode" value="custom">
|
||||
<label for="theme-mode-custom">Custom Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color Settings Card -->
|
||||
<div id="custom-theme-settings" class="card" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Custom Colors</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="primary-color">Primary Color:</label>
|
||||
<div class="color-picker-container">
|
||||
<input type="color" id="primary-color" value="#5865F2">
|
||||
<input type="text" id="primary-color-text" value="#5865F2" class="color-text-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="secondary-color">Secondary Color:</label>
|
||||
<div class="color-picker-container">
|
||||
<input type="color" id="secondary-color" value="#2D3748">
|
||||
<input type="text" id="secondary-color-text" value="#2D3748" class="color-text-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="accent-color">Accent Color:</label>
|
||||
<div class="color-picker-container">
|
||||
<input type="color" id="accent-color" value="#7289DA">
|
||||
<input type="text" id="accent-color-text" value="#7289DA" class="color-text-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="font-family">Font Family:</label>
|
||||
<select id="font-family" class="w-full">
|
||||
<option value="Inter, sans-serif">Inter</option>
|
||||
<option value="'Roboto', sans-serif">Roboto</option>
|
||||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||||
<option value="'Montserrat', sans-serif">Montserrat</option>
|
||||
<option value="'Poppins', sans-serif">Poppins</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="custom-css">Custom CSS (Advanced):</label>
|
||||
<textarea id="custom-css" rows="6" class="w-full" placeholder="Enter custom CSS here..."></textarea>
|
||||
<small class="text-muted">Custom CSS will be applied to the dashboard. Use with caution.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Theme Preview</h3>
|
||||
</div>
|
||||
<div id="theme-preview" class="theme-preview">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">Header</div>
|
||||
<div class="preview-button">Button</div>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div class="preview-card">
|
||||
<div class="preview-card-header">Card Title</div>
|
||||
<div class="preview-card-body">
|
||||
<p>This is a preview of how your theme will look.</p>
|
||||
<div class="preview-form-control"></div>
|
||||
<div class="preview-button-primary">Primary Button</div>
|
||||
<div class="preview-button-secondary">Secondary Button</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="card">
|
||||
<div class="btn-group">
|
||||
<button id="save-theme-settings-button" class="btn btn-primary">Save Theme Settings</button>
|
||||
<button id="reset-theme-settings-button" class="btn btn-warning">Reset to Defaults</button>
|
||||
</div>
|
||||
<p id="theme-settings-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -54,7 +54,7 @@ class GuildCommandSyncer:
|
||||
|
||||
async def prepare_guild_commands(self, guild_id: int) -> List[app_commands.Command]:
|
||||
"""
|
||||
Prepare guild-specific commands with customized names.
|
||||
Prepare guild-specific commands with customized names and descriptions.
|
||||
Returns a list of commands with guild-specific customizations applied.
|
||||
"""
|
||||
# Get all global commands
|
||||
@ -69,12 +69,18 @@ class GuildCommandSyncer:
|
||||
if not customizations:
|
||||
return global_commands # No customizations, use global commands
|
||||
|
||||
# Create guild-specific commands with custom names
|
||||
# Create guild-specific commands with custom names and descriptions
|
||||
guild_commands = []
|
||||
for cmd in global_commands:
|
||||
# Set guild_id attribute for use in _create_custom_command
|
||||
setattr(cmd, 'guild_id', guild_id)
|
||||
|
||||
if cmd.name in customizations:
|
||||
# Create a copy of the command with the custom name
|
||||
custom_name = customizations[cmd.name]
|
||||
# Get the custom name
|
||||
custom_data = customizations[cmd.name]
|
||||
custom_name = custom_data.get('name', cmd.name)
|
||||
|
||||
# Create a copy of the command with the custom name and description
|
||||
custom_cmd = self._create_custom_command(cmd, custom_name)
|
||||
guild_commands.append(custom_cmd)
|
||||
else:
|
||||
@ -91,14 +97,23 @@ class GuildCommandSyncer:
|
||||
|
||||
def _create_custom_command(self, original_cmd: app_commands.Command, custom_name: str) -> app_commands.Command:
|
||||
"""
|
||||
Create a copy of a command with a custom name.
|
||||
Create a copy of a command with a custom name and description.
|
||||
This is a simplified version - in practice, you'd need to handle all command attributes.
|
||||
"""
|
||||
# For simplicity, we're just creating a basic copy with the custom name
|
||||
# Get custom description if available
|
||||
custom_description = None
|
||||
if hasattr(original_cmd, 'guild_id') and original_cmd.guild_id:
|
||||
# This is a guild-specific command, get the custom description
|
||||
custom_description = asyncio.run_coroutine_threadsafe(
|
||||
settings_manager.get_custom_command_description(original_cmd.guild_id, original_cmd.name),
|
||||
asyncio.get_event_loop()
|
||||
).result()
|
||||
|
||||
# For simplicity, we're just creating a basic copy with the custom name and description
|
||||
# In a real implementation, you'd need to handle all command attributes and options
|
||||
custom_cmd = app_commands.Command(
|
||||
name=custom_name,
|
||||
description=original_cmd.description,
|
||||
description=custom_description or original_cmd.description,
|
||||
callback=original_cmd.callback
|
||||
)
|
||||
|
||||
@ -117,6 +132,11 @@ class GuildCommandSyncer:
|
||||
# Prepare guild-specific commands
|
||||
guild_commands = await self.prepare_guild_commands(guild.id)
|
||||
|
||||
# Set the commands for this guild
|
||||
self.bot.tree.clear_commands(guild=guild)
|
||||
for cmd in guild_commands:
|
||||
self.bot.tree.add_command(cmd, guild=guild)
|
||||
|
||||
# Sync commands with Discord
|
||||
synced = await self.bot.tree.sync(guild=guild)
|
||||
|
||||
|
20
main.py
20
main.py
@ -201,6 +201,9 @@ async def on_command_error(ctx, error):
|
||||
elif isinstance(error, CommandPermissionError):
|
||||
await ctx.send(str(error), ephemeral=True) # Send the error message from the exception
|
||||
log.warning(f"Command '{ctx.command.qualified_name}' blocked for user {ctx.author.id} in guild {ctx.guild.id}: {error}")
|
||||
elif isinstance(error, CommandDisabledError):
|
||||
await ctx.send(str(error), ephemeral=True) # Send the error message from the exception
|
||||
log.warning(f"Command '{ctx.command.qualified_name}' blocked for user {ctx.author.id} in guild {ctx.guild.id}: {error}")
|
||||
else:
|
||||
# Pass other errors to the original handler
|
||||
await handle_error(ctx, error)
|
||||
@ -232,6 +235,12 @@ class CommandPermissionError(commands.CheckFailure):
|
||||
self.command_name = command_name
|
||||
super().__init__(f"You do not have the required role to use the command `{command_name}`.")
|
||||
|
||||
class CommandDisabledError(commands.CheckFailure):
|
||||
"""Custom exception for disabled commands."""
|
||||
def __init__(self, command_name):
|
||||
self.command_name = command_name
|
||||
super().__init__(f"The command `{command_name}` is disabled in this server.")
|
||||
|
||||
@bot.before_invoke
|
||||
async def global_command_checks(ctx: commands.Context):
|
||||
"""Global check run before any command invocation."""
|
||||
@ -268,7 +277,14 @@ async def global_command_checks(ctx: commands.Context):
|
||||
log.warning(f"Command '{command_name}' blocked in guild {guild_id}: Cog '{cog_name}' is disabled.")
|
||||
raise CogDisabledError(cog_name)
|
||||
|
||||
# 2. Check command permissions based on roles
|
||||
# 2. Check if the Command is enabled
|
||||
# This only applies if the command has been explicitly disabled
|
||||
is_cmd_enabled = await settings_manager.is_command_enabled(guild_id, command_name, default_enabled=True)
|
||||
if not is_cmd_enabled:
|
||||
log.warning(f"Command '{command_name}' blocked in guild {guild_id}: Command is disabled.")
|
||||
raise CommandDisabledError(command_name)
|
||||
|
||||
# 3. Check command permissions based on roles
|
||||
# This check only applies if specific permissions HAVE been set for this command.
|
||||
# If no permissions are set in the DB, check_command_permission returns True.
|
||||
has_perm = await settings_manager.check_command_permission(guild_id, command_name, member_roles_ids)
|
||||
@ -276,7 +292,7 @@ async def global_command_checks(ctx: commands.Context):
|
||||
log.warning(f"Command '{command_name}' blocked for user {ctx.author.id} in guild {guild_id}: Insufficient role permissions.")
|
||||
raise CommandPermissionError(command_name)
|
||||
|
||||
# If both checks pass, the command proceeds.
|
||||
# If all checks pass, the command proceeds.
|
||||
log.debug(f"Command '{command_name}' passed global checks for user {ctx.author.id} in guild {guild_id}.")
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ import redis.asyncio as redis
|
||||
import os
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
from typing import Dict
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '.env'))
|
||||
@ -138,6 +139,17 @@ async def initialize_database():
|
||||
);
|
||||
""")
|
||||
|
||||
# Enabled Commands table - Stores the explicit enabled/disabled state for individual commands
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS enabled_commands (
|
||||
guild_id BIGINT NOT NULL,
|
||||
command_name TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
PRIMARY KEY (guild_id, command_name),
|
||||
FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE
|
||||
);
|
||||
""")
|
||||
|
||||
# Command Permissions table (simple role-based for now)
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS command_permissions (
|
||||
@ -149,12 +161,13 @@ async def initialize_database():
|
||||
);
|
||||
""")
|
||||
|
||||
# Command Customization table - Stores guild-specific command names
|
||||
# Command Customization table - Stores guild-specific command names and descriptions
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS command_customization (
|
||||
guild_id BIGINT NOT NULL,
|
||||
original_command_name TEXT NOT NULL,
|
||||
custom_command_name TEXT NOT NULL,
|
||||
custom_command_description TEXT,
|
||||
PRIMARY KEY (guild_id, original_command_name),
|
||||
FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE
|
||||
);
|
||||
@ -428,6 +441,119 @@ async def set_cog_enabled(guild_id: int, cog_name: str, enabled: bool):
|
||||
log.exception(f"Failed to invalidate Redis cache for cog enabled status '{cog_name}' (Guild: {guild_id}): {redis_err}")
|
||||
return False
|
||||
|
||||
|
||||
async def is_command_enabled(guild_id: int, command_name: str, default_enabled: bool = True) -> bool:
|
||||
"""Checks if a command is enabled for a guild, checking cache first.
|
||||
Uses default_enabled if no specific setting is found."""
|
||||
if not pg_pool or not redis_pool:
|
||||
log.warning(f"Pools not initialized, returning default for command '{command_name}'.")
|
||||
return default_enabled
|
||||
|
||||
cache_key = _get_redis_key(guild_id, "cmd_enabled", command_name)
|
||||
try:
|
||||
cached_value = await redis_pool.get(cache_key)
|
||||
if cached_value is not None:
|
||||
log.debug(f"Cache hit for command enabled status '{command_name}' (Guild: {guild_id})")
|
||||
return cached_value == "True" # Redis stores strings
|
||||
except Exception as e:
|
||||
log.exception(f"Redis error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
|
||||
|
||||
log.debug(f"Cache miss for command enabled status '{command_name}' (Guild: {guild_id})")
|
||||
db_enabled_status = None
|
||||
try:
|
||||
async with pg_pool.acquire() as conn:
|
||||
db_enabled_status = await conn.fetchval(
|
||||
"SELECT enabled FROM enabled_commands WHERE guild_id = $1 AND command_name = $2",
|
||||
guild_id, command_name
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f"Database error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
|
||||
# Fallback to default on DB error after cache miss
|
||||
return default_enabled
|
||||
|
||||
final_status = db_enabled_status if db_enabled_status is not None else default_enabled
|
||||
|
||||
# Cache the result (True or False)
|
||||
try:
|
||||
await redis_pool.set(cache_key, str(final_status), ex=3600) # Cache for 1 hour
|
||||
except Exception as e:
|
||||
log.exception(f"Redis error setting cache for command enabled status '{command_name}' (Guild: {guild_id}): {e}")
|
||||
|
||||
return final_status
|
||||
|
||||
|
||||
async def set_command_enabled(guild_id: int, command_name: str, enabled: bool):
|
||||
"""Sets the enabled status for a command in a guild and updates the cache."""
|
||||
if not pg_pool or not redis_pool:
|
||||
log.error(f"Pools not initialized, cannot set command enabled status for '{command_name}'.")
|
||||
return False
|
||||
|
||||
cache_key = _get_redis_key(guild_id, "cmd_enabled", command_name)
|
||||
try:
|
||||
async with pg_pool.acquire() as conn:
|
||||
# Ensure guild exists
|
||||
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT (guild_id) DO NOTHING;", guild_id)
|
||||
# Upsert the enabled status
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO enabled_commands (guild_id, command_name, enabled)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (guild_id, command_name) DO UPDATE SET enabled = $3;
|
||||
""",
|
||||
guild_id, command_name, enabled
|
||||
)
|
||||
|
||||
# Update cache
|
||||
await redis_pool.set(cache_key, str(enabled), ex=3600)
|
||||
log.info(f"Set command '{command_name}' enabled status to {enabled} for guild {guild_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
log.exception(f"Database or Redis error setting command enabled status for '{command_name}' in guild {guild_id}: {e}")
|
||||
# Attempt to invalidate cache on error
|
||||
try:
|
||||
await redis_pool.delete(cache_key)
|
||||
except Exception as redis_err:
|
||||
log.exception(f"Failed to invalidate Redis cache for command enabled status '{command_name}' (Guild: {guild_id}): {redis_err}")
|
||||
return False
|
||||
|
||||
|
||||
async def get_all_enabled_commands(guild_id: int) -> Dict[str, bool]:
|
||||
"""Gets all command enabled statuses for a guild.
|
||||
Returns a dictionary of command_name -> enabled status."""
|
||||
if not pg_pool:
|
||||
log.error(f"Database pool not initialized, cannot get command enabled statuses for guild {guild_id}.")
|
||||
return {}
|
||||
|
||||
try:
|
||||
async with pg_pool.acquire() as conn:
|
||||
records = await conn.fetch(
|
||||
"SELECT command_name, enabled FROM enabled_commands WHERE guild_id = $1",
|
||||
guild_id
|
||||
)
|
||||
return {record['command_name']: record['enabled'] for record in records}
|
||||
except Exception as e:
|
||||
log.exception(f"Database error getting command enabled statuses for guild {guild_id}: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
async def get_all_enabled_cogs(guild_id: int) -> Dict[str, bool]:
|
||||
"""Gets all cog enabled statuses for a guild.
|
||||
Returns a dictionary of cog_name -> enabled status."""
|
||||
if not pg_pool:
|
||||
log.error(f"Database pool not initialized, cannot get cog enabled statuses for guild {guild_id}.")
|
||||
return {}
|
||||
|
||||
try:
|
||||
async with pg_pool.acquire() as conn:
|
||||
records = await conn.fetch(
|
||||
"SELECT cog_name, enabled FROM enabled_cogs WHERE guild_id = $1",
|
||||
guild_id
|
||||
)
|
||||
return {record['cog_name']: record['enabled'] for record in records}
|
||||
except Exception as e:
|
||||
log.exception(f"Database error getting cog enabled statuses for guild {guild_id}: {e}")
|
||||
return {}
|
||||
|
||||
# --- Command Permission Functions ---
|
||||
|
||||
async def add_command_permission(guild_id: int, command_name: str, role_id: int) -> bool:
|
||||
@ -680,6 +806,39 @@ async def get_custom_command_name(guild_id: int, original_command_name: str) ->
|
||||
return custom_name
|
||||
|
||||
|
||||
async def get_custom_command_description(guild_id: int, original_command_name: str) -> str | None:
|
||||
"""Gets the custom command description for a guild, checking cache first.
|
||||
Returns None if no custom description is set."""
|
||||
if not pg_pool or not redis_pool:
|
||||
log.warning(f"Pools not initialized, returning None for custom command description '{original_command_name}'.")
|
||||
return None
|
||||
|
||||
cache_key = _get_redis_key(guild_id, "cmd_desc", original_command_name)
|
||||
try:
|
||||
cached_value = await redis_pool.get(cache_key)
|
||||
if cached_value is not None:
|
||||
log.debug(f"Cache hit for custom command description '{original_command_name}' (Guild: {guild_id})")
|
||||
return None if cached_value == "__NONE__" else cached_value
|
||||
except Exception as e:
|
||||
log.exception(f"Redis error getting custom command description for '{original_command_name}' (Guild: {guild_id}): {e}")
|
||||
|
||||
log.debug(f"Cache miss for custom command description '{original_command_name}' (Guild: {guild_id})")
|
||||
async with pg_pool.acquire() as conn:
|
||||
custom_desc = await conn.fetchval(
|
||||
"SELECT custom_command_description FROM command_customization WHERE guild_id = $1 AND original_command_name = $2",
|
||||
guild_id, original_command_name
|
||||
)
|
||||
|
||||
# Cache the result (even if None)
|
||||
try:
|
||||
value_to_cache = custom_desc if custom_desc is not None else "__NONE__"
|
||||
await redis_pool.set(cache_key, value_to_cache, ex=3600) # Cache for 1 hour
|
||||
except Exception as e:
|
||||
log.exception(f"Redis error setting cache for custom command description '{original_command_name}' (Guild: {guild_id}): {e}")
|
||||
|
||||
return custom_desc
|
||||
|
||||
|
||||
async def set_custom_command_name(guild_id: int, original_command_name: str, custom_command_name: str | None) -> bool:
|
||||
"""Sets a custom command name for a guild and updates the cache.
|
||||
Setting custom_command_name to None removes the customization."""
|
||||
@ -727,6 +886,74 @@ async def set_custom_command_name(guild_id: int, original_command_name: str, cus
|
||||
return False
|
||||
|
||||
|
||||
async def set_custom_command_description(guild_id: int, original_command_name: str, custom_command_description: str | None) -> bool:
|
||||
"""Sets a custom command description for a guild and updates the cache.
|
||||
Setting custom_command_description to None removes the description."""
|
||||
if not pg_pool or not redis_pool:
|
||||
log.error(f"Pools not initialized, cannot set custom command description for '{original_command_name}'.")
|
||||
return False
|
||||
|
||||
cache_key = _get_redis_key(guild_id, "cmd_desc", original_command_name)
|
||||
try:
|
||||
async with pg_pool.acquire() as conn:
|
||||
# Ensure guild exists
|
||||
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT (guild_id) DO NOTHING;", guild_id)
|
||||
|
||||
# Check if the command customization exists
|
||||
exists = await conn.fetchval(
|
||||
"SELECT 1 FROM command_customization WHERE guild_id = $1 AND original_command_name = $2",
|
||||
guild_id, original_command_name
|
||||
)
|
||||
|
||||
if custom_command_description is not None:
|
||||
if exists:
|
||||
# Update the existing record
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE command_customization
|
||||
SET custom_command_description = $3
|
||||
WHERE guild_id = $1 AND original_command_name = $2;
|
||||
""",
|
||||
guild_id, original_command_name, custom_command_description
|
||||
)
|
||||
else:
|
||||
# Insert a new record with default custom_command_name (same as original)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO command_customization (guild_id, original_command_name, custom_command_name, custom_command_description)
|
||||
VALUES ($1, $2, $2, $3);
|
||||
""",
|
||||
guild_id, original_command_name, custom_command_description
|
||||
)
|
||||
# Update cache
|
||||
await redis_pool.set(cache_key, custom_command_description, ex=3600)
|
||||
log.info(f"Set custom command description for '{original_command_name}' for guild {guild_id}")
|
||||
else:
|
||||
if exists:
|
||||
# Update the existing record to remove the description
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE command_customization
|
||||
SET custom_command_description = NULL
|
||||
WHERE guild_id = $1 AND original_command_name = $2;
|
||||
""",
|
||||
guild_id, original_command_name
|
||||
)
|
||||
# Update cache to indicate no description
|
||||
await redis_pool.set(cache_key, "__NONE__", ex=3600)
|
||||
log.info(f"Removed custom command description for '{original_command_name}' for guild {guild_id}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
log.exception(f"Database or Redis error setting custom command description for '{original_command_name}' in guild {guild_id}: {e}")
|
||||
# Attempt to invalidate cache on error
|
||||
try:
|
||||
await redis_pool.delete(cache_key)
|
||||
except Exception as redis_err:
|
||||
log.exception(f"Failed to invalidate Redis cache for custom command description '{original_command_name}' (Guild: {guild_id}): {redis_err}")
|
||||
return False
|
||||
|
||||
|
||||
async def get_custom_group_name(guild_id: int, original_group_name: str) -> str | None:
|
||||
"""Gets the custom command group name for a guild, checking cache first.
|
||||
Returns None if no custom name is set."""
|
||||
@ -923,19 +1150,26 @@ async def get_command_aliases(guild_id: int, original_command_name: str) -> list
|
||||
return None # Indicate error
|
||||
|
||||
|
||||
async def get_all_command_customizations(guild_id: int) -> dict[str, str] | None:
|
||||
async def get_all_command_customizations(guild_id: int) -> dict[str, dict[str, str]] | None:
|
||||
"""Gets all command customizations for a guild.
|
||||
Returns a dictionary mapping original command names to custom names, or None on error."""
|
||||
Returns a dictionary mapping original command names to a dict with 'name' and 'description' keys,
|
||||
or None on error."""
|
||||
if not pg_pool:
|
||||
log.error("Pools not initialized, cannot get command customizations.")
|
||||
return None
|
||||
try:
|
||||
async with pg_pool.acquire() as conn:
|
||||
records = await conn.fetch(
|
||||
"SELECT original_command_name, custom_command_name FROM command_customization WHERE guild_id = $1",
|
||||
"SELECT original_command_name, custom_command_name, custom_command_description FROM command_customization WHERE guild_id = $1",
|
||||
guild_id
|
||||
)
|
||||
customizations = {record['original_command_name']: record['custom_command_name'] for record in records}
|
||||
customizations = {}
|
||||
for record in records:
|
||||
cmd_name = record['original_command_name']
|
||||
customizations[cmd_name] = {
|
||||
'name': record['custom_command_name'],
|
||||
'description': record['custom_command_description']
|
||||
}
|
||||
log.debug(f"Fetched {len(customizations)} command customizations for guild {guild_id}.")
|
||||
return customizations
|
||||
except Exception as e:
|
||||
|
Loading…
x
Reference in New Issue
Block a user