Updated WebUI with Radzen and tailwind css
This commit is contained in:
@@ -1,31 +1,49 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
<PageTitle>Error - Seth Bot</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
<RadzenStack AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Gap="1.5rem" class="animate-fade-in" Style="min-height: 70vh;">
|
||||
<div class="sidebar-logo-icon" style="width: 80px; height: 80px; background: linear-gradient(135deg, #ef4444, #f43f5e);">
|
||||
<RadzenIcon Icon="error" Style="font-size: 2.5rem;" />
|
||||
</div>
|
||||
|
||||
<RadzenText TextStyle="TextStyle.H3" TextAlign="TextAlign.Center"><strong>Oops!</strong></RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" TextAlign="TextAlign.Center" class="rz-color-secondary">An error occurred while processing your request.</RadzenText>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<RadzenCard class="card-modern" Style="padding: 1rem;">
|
||||
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary">Request ID</RadzenText>
|
||||
<code>@RequestId</code>
|
||||
</RadzenCard>
|
||||
}
|
||||
|
||||
<RadzenCard class="card-modern" Style="max-width: 500px;">
|
||||
<RadzenStack Gap="0.75rem">
|
||||
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0.5rem">
|
||||
<RadzenIcon Icon="info" Style="color: var(--rz-info);" />
|
||||
<RadzenText><strong>Development Mode</strong></RadzenText>
|
||||
</RadzenStack>
|
||||
<RadzenText TextStyle="TextStyle.Body2" class="rz-color-secondary">
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error.
|
||||
</RadzenText>
|
||||
<RadzenAlert AlertStyle="AlertStyle.Warning" Variant="Variant.Flat" Shade="Shade.Lighter" Size="AlertSize.Small">
|
||||
The Development environment shouldn't be enabled for deployed applications as it may expose sensitive information.
|
||||
</RadzenAlert>
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
|
||||
<RadzenButton Text="Back to Home"
|
||||
Icon="home"
|
||||
ButtonStyle="ButtonStyle.Primary"
|
||||
Click="@(() => Navigation.NavigateTo("/"))"
|
||||
class="btn-gradient-primary" />
|
||||
</RadzenStack>
|
||||
|
||||
@code{
|
||||
[CascadingParameter] private HttpContext? HttpContext { get; set; }
|
||||
[Inject] private NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
@using DiscordBotCore.Bot
|
||||
@using DiscordBotCore.Logging
|
||||
@using DiscordBotCore.PluginManagement.Loading
|
||||
@using WebUI.Models
|
||||
@using WebUI.Services
|
||||
@implements IDisposable
|
||||
|
||||
@inject IDiscordBotApplication DiscordBotApplication
|
||||
@inject IPluginLoader PluginLoader
|
||||
@@ -13,73 +12,132 @@
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<link rel="stylesheet" href="Components/Pages/Home.css" />
|
||||
<div class="container-fluid d-flex flex-column p-3" style="height: 95vh;">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-success"
|
||||
@onclick="StartApplication"
|
||||
disabled="@IsRunning">
|
||||
<i class="bi bi-play-fill me-1"></i> Start Application
|
||||
</button>
|
||||
<PageTitle>Home - Seth Bot</PageTitle>
|
||||
|
||||
<button class="btn btn-danger"
|
||||
@onclick="StopApplication"
|
||||
disabled="@(!IsRunning)">
|
||||
<i class="bi bi-stop-fill me-1"></i> Stop Application
|
||||
</button>
|
||||
|
||||
<button class="btn btn-warning text-dark"
|
||||
@onclick="LoadPlugins"
|
||||
disabled="@(!IsRunning)">
|
||||
<i class="bi bi-plug-fill me-1"></i> Load Plugins
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary"
|
||||
@onclick="ClearLogs">
|
||||
<i class="bi bi-trash3-fill me-1"></i> Clear Logs
|
||||
</button>
|
||||
<div class="space-y-6">
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl md:text-3xl font-bold text-white mb-1">Dashboard</h2>
|
||||
<p class="text-slate-400 text-sm">Monitor and control your Discord bot</p>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium @(IsRunning ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400")">
|
||||
<span class="w-2 h-2 rounded-full @(IsRunning ? "bg-emerald-400" : "bg-red-400")"></span>
|
||||
@(IsRunning ? "Online" : "Offline")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Console Log</h5>
|
||||
<span class="badge bg-secondary">@Logger.LogMessages.Count()</span>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="p-2 rounded-lg bg-primary/20">
|
||||
<RadzenIcon Icon="tune" class="text-primary" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white">Control Panel</h3>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<RadzenButton Text="Start Bot"
|
||||
Icon="play_arrow"
|
||||
ButtonStyle="ButtonStyle.Success"
|
||||
Disabled="@IsRunning"
|
||||
Click="@StartApplication"
|
||||
class="!bg-gradient-to-r !from-emerald-500 !to-teal-500" />
|
||||
|
||||
<RadzenButton Text="Stop Bot"
|
||||
Icon="stop"
|
||||
ButtonStyle="ButtonStyle.Danger"
|
||||
Disabled="@(!IsRunning)"
|
||||
Click="@StopApplication"
|
||||
class="!bg-gradient-to-r !from-red-500 !to-rose-500" />
|
||||
|
||||
<RadzenButton Text="Load Plugins"
|
||||
Icon="extension"
|
||||
ButtonStyle="ButtonStyle.Warning"
|
||||
Disabled="@(!IsRunning)"
|
||||
Click="@LoadPlugins"
|
||||
class="!bg-gradient-to-r !from-amber-500 !to-orange-500" />
|
||||
|
||||
<RadzenButton Text="Clear Logs"
|
||||
Icon="delete_sweep"
|
||||
ButtonStyle="ButtonStyle.Light"
|
||||
Click="@ClearLogs"
|
||||
class="!bg-slate-700 hover:!bg-slate-600 !border-slate-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="consoleLog"
|
||||
class="bg-dark text-white p-3 flex-grow-1 overflow-auto"
|
||||
style="font-family:monospace;">
|
||||
@foreach (var line in Logger.LogMessages)
|
||||
{
|
||||
<div style="@GetLogStyle(line)">@line.Message</div>
|
||||
}
|
||||
|
||||
<!-- Console Log -->
|
||||
<div class="console-container bg-[#0d1117] border border-[#21262d] rounded-2xl overflow-hidden">
|
||||
<div class="bg-[#161b22] px-5 py-4 border-b border-[#21262d] flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-primary/20">
|
||||
<RadzenIcon Icon="terminal" class="text-primary-light" />
|
||||
</div>
|
||||
<h3 class="text-white font-semibold">Console Log</h3>
|
||||
</div>
|
||||
<span class="px-3 py-1 text-xs font-medium rounded-full bg-slate-800 text-slate-300">
|
||||
@_logCount entries
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="consoleLog" class="p-4 font-mono text-sm leading-relaxed max-h-[400px] overflow-y-auto">
|
||||
@if (_logCount == 0)
|
||||
{
|
||||
<div class="flex flex-col items-center justify-center h-[200px] text-slate-500">
|
||||
<RadzenIcon Icon="inbox" class="text-5xl opacity-50 mb-2" />
|
||||
<p>No logs yet. Start the bot to see activity.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var line in Logger.LogMessages)
|
||||
{
|
||||
<div @key="line.GetHashCode()" class="flex items-start gap-3 py-1 px-2 rounded hover:bg-white/5">
|
||||
<span class="text-slate-500 text-xs min-w-[50px] shrink-0 pt-0.5">@line.ThrowTime.ToString("HH:mm:ss")</span>
|
||||
<span class="shrink-0 w-4 text-center @GetLogTailwindClass(line)">@GetLogIcon(line)</span>
|
||||
<span class="flex-1 break-all @GetLogTailwindClass(line)">@line.Message</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.scrollToBottom = function (elementId) {
|
||||
var el = document.getElementById(elementId);
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@code {
|
||||
private bool IsRunning { get; set; }
|
||||
private int _logCount;
|
||||
private System.Timers.Timer? _debounceTimer;
|
||||
private bool _pendingUpdate;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
IsRunning = DiscordBotApplication.IsReady;
|
||||
_logCount = Logger.LogMessages.Count();
|
||||
Logger.OnLogReceived += LoggerOnLogReceived;
|
||||
|
||||
// Debounce timer for log updates - prevents too many re-renders
|
||||
_debounceTimer = new System.Timers.Timer(100);
|
||||
_debounceTimer.Elapsed += async (s, e) =>
|
||||
{
|
||||
if (_pendingUpdate)
|
||||
{
|
||||
_pendingUpdate = false;
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
_logCount = Logger.LogMessages.Count();
|
||||
StateHasChanged();
|
||||
JS.InvokeVoidAsync("scrollToBottom", "consoleLog");
|
||||
});
|
||||
}
|
||||
};
|
||||
_debounceTimer.AutoReset = true;
|
||||
_debounceTimer.Start();
|
||||
}
|
||||
|
||||
private void LoggerOnLogReceived(ILogMessage obj)
|
||||
{
|
||||
InvokeAsync(async () =>
|
||||
{
|
||||
await JS.InvokeVoidAsync("scrollToBottom", "consoleLog");
|
||||
StateHasChanged();
|
||||
});
|
||||
_pendingUpdate = true;
|
||||
}
|
||||
|
||||
private async Task StartApplication()
|
||||
@@ -88,7 +146,7 @@
|
||||
{
|
||||
await DiscordBotApplication.StartAsync();
|
||||
Logger.Log("Application started", this);
|
||||
NotificationService.Notify("Bot Started !", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Bot Started!");
|
||||
}
|
||||
|
||||
IsRunning = DiscordBotApplication.IsReady;
|
||||
@@ -100,7 +158,7 @@
|
||||
{
|
||||
await DiscordBotApplication.StopAsync();
|
||||
Logger.Log("Application stopped", this);
|
||||
NotificationService.Notify("Bot Stopped !", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Bot Stopped!");
|
||||
}
|
||||
|
||||
IsRunning = DiscordBotApplication.IsReady;
|
||||
@@ -111,24 +169,43 @@
|
||||
Logger.Log("Loading plugins", this);
|
||||
await PluginLoader.LoadPlugins();
|
||||
Logger.Log("Plugins loaded", this);
|
||||
NotificationService.Notify("Plugins Loaded !", NotificationType.Success);
|
||||
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Plugins Loaded!");
|
||||
}
|
||||
|
||||
private string GetLogStyle(ILogMessage logMessage)
|
||||
private static string GetLogTailwindClass(ILogMessage logMessage)
|
||||
{
|
||||
return logMessage.LogMessageType switch
|
||||
{
|
||||
LogType.Info => "color: white;",
|
||||
LogType.Warning => "color: yellow;",
|
||||
LogType.Error => "color: red;",
|
||||
LogType.Critical => "color: purple;",
|
||||
_ => ""
|
||||
LogType.Info => "text-sky-400",
|
||||
LogType.Warning => "text-amber-400",
|
||||
LogType.Error => "text-red-400",
|
||||
LogType.Critical => "text-purple-400 font-semibold",
|
||||
_ => "text-sky-400"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetLogIcon(ILogMessage logMessage)
|
||||
{
|
||||
return logMessage.LogMessageType switch
|
||||
{
|
||||
LogType.Info => "ℹ",
|
||||
LogType.Warning => "⚠",
|
||||
LogType.Error => "✕",
|
||||
LogType.Critical => "☠",
|
||||
_ => "•"
|
||||
};
|
||||
}
|
||||
|
||||
private void ClearLogs()
|
||||
{
|
||||
Logger.LogMessages.Clear();
|
||||
_logCount = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.OnLogReceived -= LoggerOnLogReceived;
|
||||
_debounceTimer?.Stop();
|
||||
_debounceTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,74 +5,181 @@
|
||||
@using DiscordBotCore.Logging
|
||||
@using DiscordBotCore.PluginManagement
|
||||
@using DiscordBotCore.PluginManagement.Models
|
||||
@using WebUI.Models
|
||||
@using WebUI.Services
|
||||
@using WebUI.Components.Shared
|
||||
|
||||
@inject NavigationManager Navigation
|
||||
@inject NotificationService NotificationService
|
||||
|
||||
@inject IPluginManager PluginManager
|
||||
@inject IConfiguration Configuration
|
||||
@inject ILogger Logger
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<CenteredCard Title="Upload plugin">
|
||||
<EditForm Model="@pluginModel" OnValidSubmit="HandleValidSubmit" FormName="pluginForm">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
<PageTitle>Add Local Plugin - Seth Bot</PageTitle>
|
||||
|
||||
<div class="form-group">
|
||||
<ModernLabel Text="Plugin Name"></ModernLabel>
|
||||
<RoundedTextBox @bind-Value="pluginModel.Name" Placeholder="Example Plugin"></RoundedTextBox>
|
||||
<div class="max-w-xl mx-auto space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-4">
|
||||
<div class="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-xl shadow-emerald-500/40">
|
||||
<RadzenIcon Icon="upload" class="text-white text-3xl" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<ModernLabel Text="Plugin version"></ModernLabel>
|
||||
<RoundedTextBox @bind-Value="pluginModel.Version" Placeholder="1.0.0"></RoundedTextBox>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Add Local Plugin</h2>
|
||||
<p class="text-slate-400 text-sm">Upload a plugin file from your computer</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<ModernCheckbox @bind-Checked="pluginModel.IsEnabled" Label="Enable Plugin"></ModernCheckbox>
|
||||
</div>
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-6">
|
||||
<RadzenTemplateForm TItem="PluginUploadModel" Data="@_pluginModel" Submit="@HandleValidSubmit">
|
||||
<div class="space-y-6">
|
||||
<!-- Plugin Name -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-slate-300">Plugin Name</label>
|
||||
<RadzenTextBox @bind-Value="@_pluginModel.Name"
|
||||
Name="Name"
|
||||
Placeholder="My Awesome Plugin"
|
||||
class="!w-full !bg-dark-tertiary !border-slate-600 hover:!border-primary focus-within:!border-primary !rounded-xl" />
|
||||
<RadzenRequiredValidator Component="Name" Text="Plugin name is required" class="!text-red-400 !text-xs" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<ModernFileUploader OnFileUploaded="HandleUploadedFile" AllowedFileTypes="@allowedFileTypes"/>
|
||||
@if (_message is not null)
|
||||
{
|
||||
<p class="mt-2 text-green-600">@_message</p>
|
||||
}
|
||||
<!-- Plugin Version -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-slate-300">Version</label>
|
||||
<RadzenTextBox @bind-Value="@_pluginModel.Version"
|
||||
Name="Version"
|
||||
Placeholder="1.0.0"
|
||||
class="!w-full !bg-dark-tertiary !border-slate-600 hover:!border-primary focus-within:!border-primary !rounded-xl !font-mono !transition-all !duration-300" />
|
||||
<p class="text-slate-500 text-xs">Format: major.minor.patch (e.g., 1.0.0)</p>
|
||||
<RadzenRequiredValidator Component="Version" Text="Version is required" class="!text-red-400 !text-xs" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Enable Plugin Toggle -->
|
||||
<div class="p-4 rounded-xl border border-slate-700/50 bg-dark-tertiary/30">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 flex items-center justify-center shadow-md shadow-amber-500/20">
|
||||
<RadzenIcon Icon="power_settings_new" class="text-white text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-white">Enable Plugin</p>
|
||||
<p class="text-slate-400 text-xs">Activate this plugin after upload</p>
|
||||
</div>
|
||||
</div>
|
||||
<RadzenSwitch @bind-Value="@_pluginModel.IsEnabled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" type="submit">Upload</button>
|
||||
</EditForm>
|
||||
</CenteredCard>
|
||||
<!-- File Upload -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-slate-300">Plugin File (.dll)</label>
|
||||
<div @onclick="TriggerFileInput" class="cursor-pointer">
|
||||
<div class="@FileUploadCardClass">
|
||||
<div class="flex flex-col items-center gap-4 py-6">
|
||||
@if (_selectedFile != null)
|
||||
{
|
||||
<div class="w-14 h-14 rounded-xl bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-lg shadow-emerald-500/30 animate-bounce-in">
|
||||
<RadzenIcon Icon="check" class="text-white text-xl" />
|
||||
</div>
|
||||
<div class="text-center space-y-1">
|
||||
<p class="font-semibold text-emerald-400">@_selectedFile.Name</p>
|
||||
<p class="text-slate-400 text-sm">@((_selectedFile.Size / 1024.0).ToString("F1")) KB</p>
|
||||
<p class="text-slate-500 text-xs">Click to change file</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="w-14 h-14 rounded-xl bg-slate-700 flex items-center justify-center">
|
||||
<RadzenIcon Icon="cloud_upload" class="text-slate-400 text-xl" />
|
||||
</div>
|
||||
<div class="text-center space-y-1">
|
||||
<p class="font-semibold text-white">Drop your plugin file here</p>
|
||||
<p class="text-slate-400 text-sm">or click to browse</p>
|
||||
<p class="text-slate-500 text-xs mt-2">Accepts .dll files up to 10MB</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputFile id="pluginFileInput" OnChange="HandleFileChange" accept=".dll" style="display: none;" />
|
||||
|
||||
@if (!string.IsNullOrEmpty(_uploadMessage))
|
||||
{
|
||||
<div class="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
||||
@_uploadMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="button"
|
||||
@onclick="@(() => Navigation.NavigateTo("/plugins/local"))"
|
||||
class="flex-1 py-2.5 px-4 rounded-xl bg-slate-700 hover:bg-slate-600 text-white font-medium transition-all duration-200">
|
||||
Cancel
|
||||
</button>
|
||||
<RadzenButton Text="Upload Plugin"
|
||||
Icon="upload"
|
||||
ButtonType="ButtonType.Submit"
|
||||
ButtonStyle="ButtonStyle.Success"
|
||||
Disabled="@(_selectedFile == null || !IsValidVersion)"
|
||||
class="flex-1 !bg-gradient-to-r !from-emerald-500 !to-teal-500 hover:!shadow-lg hover:!shadow-emerald-500/30 !transition-all !duration-300 !rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
</RadzenTemplateForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private PluginUploadModel pluginModel = new();
|
||||
private IBrowserFile? selectedFile;
|
||||
|
||||
private List<string> allowedFileTypes { get; set; } = new List<string> { ".dll"};
|
||||
private PluginUploadModel _pluginModel = new();
|
||||
private IBrowserFile? _selectedFile;
|
||||
private string? _uploadMessage;
|
||||
private const long MaxFileSize = 10 * 1024 * 1024;
|
||||
|
||||
private string? _message;
|
||||
private bool IsValidVersion => System.Text.RegularExpressions.Regex.IsMatch(_pluginModel.Version, @"^\d+\.\d+\.\d+$");
|
||||
|
||||
private bool IsValidVersion => System.Text.RegularExpressions.Regex.IsMatch(pluginModel.Version ?? "", @"^\d+\.\d+\.\d+$");
|
||||
private string FileUploadCardClass => _selectedFile != null
|
||||
? "rounded-xl border-2 border-dashed border-emerald-500 bg-emerald-500/5 transition-all duration-300"
|
||||
: "rounded-xl border-2 border-dashed border-slate-600 bg-dark-tertiary/30 hover:border-primary/50 hover:bg-primary/5 transition-all duration-300";
|
||||
|
||||
private async Task HandleUploadedFile(IBrowserFile file)
|
||||
private async Task TriggerFileInput()
|
||||
{
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream().ReadAsync(buffer);
|
||||
_message = $"Uploaded file: {file.Name} ({file.Size} bytes)";
|
||||
selectedFile = file;
|
||||
await JS.InvokeVoidAsync("eval", "document.getElementById('pluginFileInput').click()");
|
||||
}
|
||||
|
||||
private void HandleFileChange(InputFileChangeEventArgs e)
|
||||
{
|
||||
var file = e.File;
|
||||
_uploadMessage = null;
|
||||
|
||||
if (!file.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_uploadMessage = "Invalid file type. Only .dll files are allowed.";
|
||||
_selectedFile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.Size > MaxFileSize)
|
||||
{
|
||||
_uploadMessage = "File too large. Maximum size is 10 MB.";
|
||||
_selectedFile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedFile = file;
|
||||
|
||||
// Auto-fill plugin name from file if empty
|
||||
if (string.IsNullOrEmpty(_pluginModel.Name))
|
||||
{
|
||||
_pluginModel.Name = Path.GetFileNameWithoutExtension(file.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
if (!IsValidVersion || selectedFile is null || string.IsNullOrEmpty(selectedFile.Name))
|
||||
if (!IsValidVersion || _selectedFile is null || string.IsNullOrEmpty(_selectedFile.Name))
|
||||
{
|
||||
NotificationService.Notify("Invalid field values. Please check the form.", NotificationType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", "Invalid field values. Please check the form.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,32 +188,32 @@
|
||||
if (string.IsNullOrEmpty(pluginsFolder))
|
||||
{
|
||||
Logger.Log("Plugins folder is not set in the configuration.", this, LogType.Error);
|
||||
NotificationService.Notify("Plugins folder is not set in the configuration.", NotificationType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", "Plugins folder is not set in the configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
string filePath = Path.Combine(pluginsFolder, selectedFile.Name);
|
||||
string filePath = Path.Combine(pluginsFolder, _selectedFile.Name);
|
||||
|
||||
await using var stream = selectedFile.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
|
||||
await using var stream = _selectedFile.OpenReadStream(maxAllowedSize: MaxFileSize);
|
||||
await using var fileStream = File.Create(filePath);
|
||||
await stream.CopyToAsync(fileStream);
|
||||
|
||||
LocalPlugin plugin = new LocalPlugin(pluginModel.Name, pluginModel.Version, filePath, new(), true, pluginModel.IsEnabled);
|
||||
LocalPlugin plugin = new LocalPlugin(_pluginModel.Name, _pluginModel.Version, filePath, new(), true, _pluginModel.IsEnabled);
|
||||
await PluginManager.AppendPluginToDatabase(plugin);
|
||||
|
||||
NotificationService.Notify($"Plugin {pluginModel.Name} uploaded successfully!", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {_pluginModel.Name} uploaded successfully!");
|
||||
|
||||
Navigation.NavigateTo("/");
|
||||
Navigation.NavigateTo("/plugins/local");
|
||||
}
|
||||
|
||||
public class PluginUploadModel
|
||||
private class PluginUploadModel
|
||||
{
|
||||
[Required]
|
||||
[Required(ErrorMessage = "Plugin name is required.")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[Required(ErrorMessage = "Version is required.")]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,74 +9,172 @@
|
||||
@inject IPluginManager PluginManager
|
||||
@inject ILogger Logger
|
||||
@inject IPluginLoader PluginLoader
|
||||
|
||||
@inject NavigationManager Navigation
|
||||
@inject NotificationService NotificationService
|
||||
@inject DialogService DialogService
|
||||
|
||||
<h3>Installed Plugins</h3>
|
||||
<button class="btn btn-outline-success" @onclick="NavigateToAddPlugin">
|
||||
<span class="bi me-1"></span> Add Local
|
||||
</button>
|
||||
<table class="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
<th>Offline Added</th>
|
||||
<th>Actions</th>
|
||||
@* <th> *@
|
||||
@* <button class="btn btn-outline-success" @onclick="NavigateToAddPlugin"> *@
|
||||
@* <span class="bi me-1"></span> Add Local *@
|
||||
@* </button> *@
|
||||
@* </th> *@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var plugin in _installedPlugins)
|
||||
<PageTitle>Local Plugins - Seth Bot</PageTitle>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-amber-500 to-orange-500 flex items-center justify-center shadow-lg shadow-amber-500/30">
|
||||
<RadzenIcon Icon="folder" class="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Local Plugins</h2>
|
||||
<p class="text-slate-400 text-sm">Manage your installed plugins</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadzenButton Text="Add Local Plugin"
|
||||
Icon="add"
|
||||
ButtonStyle="ButtonStyle.Success"
|
||||
Click="@NavigateToAddPlugin"
|
||||
class="!bg-gradient-to-r !from-emerald-500 !to-teal-500 !rounded-xl" />
|
||||
</div>
|
||||
|
||||
@if (_installedPlugins.Any())
|
||||
{
|
||||
<tr>
|
||||
<td>@plugin.Name</td>
|
||||
<td>@plugin.Version</td>
|
||||
<td>@(plugin.IsOfflineAdded ? "Yes" : "No")</td>
|
||||
<td >
|
||||
<button class="btn btn-danger btn-sm m-1" @onclick="async () => await DeletePluginButtonClick(plugin)">Delete</button>
|
||||
@if (!plugin.IsOfflineAdded)
|
||||
{
|
||||
<button class="btn btn-info btn-sm m-1" @onclick="async () => await PluginDetailsButtonClick(plugin)">Details</button>
|
||||
}
|
||||
</td>
|
||||
@* <td></td> *@
|
||||
</tr>
|
||||
<!-- Plugins DataGrid -->
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl overflow-hidden shadow-xl">
|
||||
<RadzenDataGrid Data="@_installedPlugins" TItem="InstalledPlugin" AllowSorting="true">
|
||||
<Columns>
|
||||
<RadzenDataGridColumn TItem="InstalledPlugin" Title="Plugin" Width="auto">
|
||||
<Template Context="plugin">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-md shadow-primary/20">
|
||||
<RadzenIcon Icon="extension" class="text-white text-sm" />
|
||||
</div>
|
||||
<span class="font-semibold text-white">@plugin.Name</span>
|
||||
</div>
|
||||
</Template>
|
||||
</RadzenDataGridColumn>
|
||||
|
||||
<RadzenDataGridColumn TItem="InstalledPlugin" Property="Version" Title="Version" Width="120px" TextAlign="TextAlign.Center">
|
||||
<Template Context="plugin">
|
||||
<span class="px-2.5 py-1 text-xs font-medium rounded-lg bg-slate-700/50 text-slate-300">v@plugin.Version</span>
|
||||
</Template>
|
||||
</RadzenDataGridColumn>
|
||||
|
||||
<RadzenDataGridColumn TItem="InstalledPlugin" Property="IsOfflineAdded" Title="Source" Width="120px" TextAlign="TextAlign.Center">
|
||||
<Template Context="plugin">
|
||||
@if (plugin.IsOfflineAdded)
|
||||
{
|
||||
<span class="px-2.5 py-1 text-xs font-medium rounded-lg bg-amber-500/15 text-amber-400">Local</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="px-2.5 py-1 text-xs font-medium rounded-lg bg-emerald-500/15 text-emerald-400">Online</span>
|
||||
}
|
||||
</Template>
|
||||
</RadzenDataGridColumn>
|
||||
|
||||
<RadzenDataGridColumn TItem="InstalledPlugin" Title="Actions" Width="150px" TextAlign="TextAlign.End">
|
||||
<Template Context="plugin">
|
||||
<div class="flex gap-2 justify-end">
|
||||
@if (!plugin.IsOfflineAdded)
|
||||
{
|
||||
<button @onclick="@(async () => await PluginDetailsButtonClick(plugin))"
|
||||
class="p-2 rounded-lg bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors duration-200">
|
||||
<RadzenIcon Icon="info" class="text-sm" />
|
||||
</button>
|
||||
}
|
||||
<button @onclick="@(async () => await DeletePluginButtonClick(plugin))"
|
||||
class="p-2 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors duration-200">
|
||||
<RadzenIcon Icon="delete" class="text-sm" />
|
||||
</button>
|
||||
</div>
|
||||
</Template>
|
||||
</RadzenDataGridColumn>
|
||||
</Columns>
|
||||
</RadzenDataGrid>
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
else
|
||||
{
|
||||
<!-- Empty State -->
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-12">
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-amber-500/30 to-orange-500/30 flex items-center justify-center opacity-60">
|
||||
<RadzenIcon Icon="extension" class="text-amber-400 text-3xl" />
|
||||
</div>
|
||||
<div class="text-center space-y-2">
|
||||
<h3 class="text-lg font-semibold text-white">No plugins installed</h3>
|
||||
<p class="text-slate-400 text-sm">Get started by installing plugins from the marketplace or adding local ones.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 justify-center">
|
||||
<RadzenButton Text="Browse Online Plugins"
|
||||
Icon="cloud_download"
|
||||
ButtonStyle="ButtonStyle.Primary"
|
||||
Click="@(() => Navigation.NavigateTo("/plugins/online"))"
|
||||
class="!bg-gradient-to-r !from-primary !to-accent hover:!shadow-lg hover:!shadow-primary/30 !transition-all !duration-300 !rounded-xl" />
|
||||
<button @onclick="NavigateToAddPlugin"
|
||||
class="px-4 py-2 rounded-xl bg-slate-700 hover:bg-slate-600 text-white font-medium transition-all duration-200 flex items-center gap-2">
|
||||
<RadzenIcon Icon="add" class="text-sm" />
|
||||
Add Local Plugin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Plugin Details Dialog -->
|
||||
@if (_showPluginDetailsModal && _selectedPluginDetails != null)
|
||||
{
|
||||
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Plugin Details: @_selectedPluginDetails.Name</h5>
|
||||
<button type="button" class="btn-close" @onclick="ClosePluginDetailsModal"></button>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl max-w-md w-full mx-4 overflow-hidden shadow-2xl animate-scale-in">
|
||||
<!-- Header -->
|
||||
<div class="bg-gradient-to-r from-pink-500 to-rose-500 p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-white/20 flex items-center justify-center backdrop-blur-sm">
|
||||
<RadzenIcon Icon="extension" class="text-white text-xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">@_selectedPluginDetails.Name</h3>
|
||||
<p class="text-white/70 text-sm">v@_selectedPluginDetails.Version</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @onclick="ClosePluginDetailsModal"
|
||||
class="p-2 rounded-lg hover:bg-white/10 text-white transition-colors duration-200">
|
||||
<RadzenIcon Icon="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>Version:</strong> @_selectedPluginDetails.Version</p>
|
||||
<p><strong>Author:</strong> @_selectedPluginDetails.Author</p>
|
||||
<p><strong>Description:</strong> @_selectedPluginDetails.Description</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="space-y-1">
|
||||
<p class="text-slate-400 text-xs uppercase tracking-wider">Author</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<RadzenIcon Icon="person" class="text-pink-400" />
|
||||
<span class="text-white">@_selectedPluginDetails.Author</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @onclick="ClosePluginDetailsModal">Close</button>
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-slate-400 text-xs uppercase tracking-wider">Description</p>
|
||||
<p class="text-white">@_selectedPluginDetails.Description</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-5 pt-0">
|
||||
<button @onclick="ClosePluginDetailsModal"
|
||||
class="w-full py-2.5 px-4 rounded-xl bg-slate-700 hover:bg-slate-600 text-white font-medium transition-all duration-200">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
private readonly List<InstalledPlugin> _installedPlugins = new List<InstalledPlugin>();
|
||||
|
||||
private readonly List<InstalledPlugin> _installedPlugins = new();
|
||||
private bool _showPluginDetailsModal;
|
||||
private OnlinePlugin? _selectedPluginDetails;
|
||||
|
||||
@@ -91,14 +189,15 @@
|
||||
if (!response.IsSuccess)
|
||||
{
|
||||
Logger.Log(response.Message, this, LogType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", response.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
_installedPlugins.RemoveAll(p => p.Name == plugin.Name);
|
||||
Logger.Log($"Plugin {plugin.Name} deleted", this);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {plugin.Name} deleted successfully!");
|
||||
|
||||
await PluginLoader.LoadPlugins();
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -121,7 +220,6 @@
|
||||
|
||||
private void ClosePluginDetailsModal()
|
||||
{
|
||||
Logger.Log("Closing plugin details modal", this);
|
||||
_showPluginDetailsModal = false;
|
||||
_selectedPluginDetails = null;
|
||||
StateHasChanged();
|
||||
@@ -156,8 +254,8 @@
|
||||
|
||||
private class InstalledPlugin
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public bool IsOfflineAdded { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,87 +4,121 @@
|
||||
@using DiscordBotCore.Logging
|
||||
@using DiscordBotCore.PluginManagement
|
||||
|
||||
@using WebUI.Models
|
||||
@using WebUI.Services
|
||||
|
||||
@inject NotificationService NotificationService
|
||||
@inject DialogService DialogService
|
||||
|
||||
<PageTitle>Online Plugins - Seth Bot</PageTitle>
|
||||
|
||||
<h3>Available Plugins</h3>
|
||||
@if (_onlinePlugins.Any())
|
||||
{
|
||||
<table class="table table-responsive text-center align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Author</th>
|
||||
<th>Version</th>
|
||||
<th>Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var plugin in _onlinePlugins)
|
||||
{
|
||||
<tr>
|
||||
<td>@plugin.Name</td>
|
||||
<td>@plugin.Description</td>
|
||||
<td>@plugin.Author</td>
|
||||
<td>@plugin.Version</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-dark" @onclick="async () => await InstallPlugin(plugin.Id)">
|
||||
<span>Download</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Loading...</p>
|
||||
}
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-lg shadow-emerald-500/30">
|
||||
<RadzenIcon Icon="cloud_download" class="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Online Plugins</h2>
|
||||
<p class="text-slate-400 text-sm">Browse and install plugins from the marketplace</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-emerald-500/15 text-emerald-400">
|
||||
@_onlinePlugins.Count available
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@if (_showInstallPercentage)
|
||||
{
|
||||
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0, 0, 0, 0.5);">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Installing Plugin...</h5>
|
||||
<button type="button" class="btn-close" @onclick="CloseInstallPercentageModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Progress: @($"{_installPercentage:F0}")%</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: @_installPercentage%"
|
||||
aria-valuenow="@_installPercentage"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
@if (_onlinePlugins.Any())
|
||||
{
|
||||
<!-- Plugins Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@foreach (var plugin in _onlinePlugins)
|
||||
{
|
||||
<div @key="plugin.Id" class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-5 hover:border-primary/50 transition-colors">
|
||||
<div class="flex flex-col h-full gap-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/30">
|
||||
<RadzenIcon Icon="extension" class="text-white" />
|
||||
</div>
|
||||
<span class="px-2.5 py-1 text-xs font-medium rounded-lg bg-slate-700/50 text-slate-300">
|
||||
v@plugin.Version
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 space-y-1">
|
||||
<h3 class="font-semibold text-white">@plugin.Name</h3>
|
||||
<p class="text-sm text-slate-400 line-clamp-2">@plugin.Description</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2 border-t border-slate-700/50">
|
||||
<div class="flex items-center gap-2 text-slate-500 text-sm">
|
||||
<RadzenIcon Icon="person" class="text-xs" />
|
||||
<span>@plugin.Author</span>
|
||||
</div>
|
||||
|
||||
<RadzenButton Text="Install"
|
||||
Icon="download"
|
||||
ButtonStyle="ButtonStyle.Primary"
|
||||
Size="ButtonSize.Small"
|
||||
Click="@(async () => await InstallPlugin(plugin.Id))"
|
||||
class="!bg-gradient-to-r !from-primary !to-accent !rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @onclick="CloseInstallPercentageModal">Cancel</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Loading State -->
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-12">
|
||||
<div class="flex flex-col items-center justify-center gap-4">
|
||||
<RadzenProgressBarCircular ShowValue="false" Mode="ProgressBarMode.Indeterminate" Size="ProgressBarCircularSize.Large" />
|
||||
<p class="text-slate-400">Loading available plugins...</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Install Progress Dialog -->
|
||||
@if (_showInstallPercentage)
|
||||
{
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl max-w-md w-full mx-4 p-6 shadow-2xl animate-scale-in">
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-xl shadow-primary/40 animate-pulse">
|
||||
<RadzenIcon Icon="cloud_download" class="text-white text-2xl" />
|
||||
</div>
|
||||
|
||||
<div class="text-center space-y-1">
|
||||
<h3 class="text-lg font-semibold text-white">Installing Plugin</h3>
|
||||
<p class="text-slate-400 text-sm">Please wait while we download and install...</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-400">Progress</span>
|
||||
<span class="font-semibold text-white">@($"{_installPercentage:F0}%")</span>
|
||||
</div>
|
||||
<div class="h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-gradient-to-r from-primary to-accent rounded-full transition-all duration-300 ease-out" style="width: @_installPercentage%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @onclick="CloseInstallPercentageModal"
|
||||
class="w-full py-2.5 px-4 rounded-xl bg-slate-700 hover:bg-slate-600 text-white font-medium transition-all duration-200">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Inject]
|
||||
public IPluginManager PluginManager { get; set; }
|
||||
|
||||
[Inject]
|
||||
public ILogger Logger { get; set; }
|
||||
[Inject] public IPluginManager PluginManager { get; set; } = default!;
|
||||
[Inject] public ILogger Logger { get; set; } = default!;
|
||||
|
||||
private bool _showInstallPercentage;
|
||||
private float _installPercentage = 0f;
|
||||
private float _installPercentage;
|
||||
|
||||
private readonly List<OnlinePluginModel> _onlinePlugins = new();
|
||||
|
||||
@@ -132,7 +166,7 @@ else
|
||||
if (pluginData is null)
|
||||
{
|
||||
Logger.Log("Plugin data is null.", this, LogType.Error);
|
||||
NotificationService.Notify("Plugin data is null.", NotificationType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", "Plugin data is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,8 +182,7 @@ else
|
||||
await PluginManager.InstallPlugin(pluginData, progress);
|
||||
|
||||
Logger.Log($"Plugin {pluginData.Name} installed successfully.", this);
|
||||
|
||||
NotificationService.Notify($"Plugin {pluginData.Name} installed successfully!", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {pluginData.Name} installed successfully!");
|
||||
|
||||
CloseInstallPercentageModal();
|
||||
StateHasChanged();
|
||||
@@ -165,9 +198,9 @@ else
|
||||
private class OnlinePluginModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -2,43 +2,112 @@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using DiscordBotCore.Configuration
|
||||
@using DiscordBotCore.Logging
|
||||
@using WebUI.Components.Shared
|
||||
@inject NavigationManager Navigation
|
||||
@inject NotificationService NotificationService
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@if (_settingsViewModel is not null)
|
||||
{
|
||||
<CenteredCard Title="Settings">
|
||||
<EditForm Model="_settingsViewModel" OnValidSubmit="HandleSubmitTask">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary class="text-danger" />
|
||||
<PageTitle>Settings - Seth Bot</PageTitle>
|
||||
|
||||
<div class="mb-3">
|
||||
<ModernLabel Text="Token"/>
|
||||
<RoundedTextBox @bind-Value="_settingsViewModel.Token" Placeholder="Your bot token"/>
|
||||
<div class="max-w-xl mx-auto space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-4">
|
||||
<div class="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-xl shadow-primary/40">
|
||||
<RadzenIcon Icon="settings" class="text-white text-3xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Settings</h2>
|
||||
<p class="text-slate-400 text-sm">Configure your Discord bot settings</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_settingsViewModel is not null)
|
||||
{
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-6">
|
||||
<RadzenTemplateForm TItem="SettingsViewModel" Data="@_settingsViewModel" Submit="@HandleSubmitTask">
|
||||
<div class="space-y-6">
|
||||
<!-- Token Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-slate-300">Bot Token</label>
|
||||
<RadzenPassword @bind-Value="@_settingsViewModel.Token"
|
||||
Name="Token"
|
||||
Placeholder="Enter your bot token"
|
||||
class="!w-full !bg-dark-tertiary !border-slate-600 hover:!border-primary focus-within:!border-primary !rounded-xl" />
|
||||
<RadzenRequiredValidator Component="Token" Text="Token is required" class="!text-red-400 !text-xs" />
|
||||
</div>
|
||||
|
||||
<!-- Prefix Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-slate-300">Command Prefix</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<RadzenTextBox @bind-Value="@_settingsViewModel.Prefix"
|
||||
Name="Prefix"
|
||||
Placeholder="!"
|
||||
MaxLength="5"
|
||||
class="!w-20 !text-center !font-mono !text-xl !bg-dark-tertiary !border-slate-600 hover:!border-primary focus-within:!border-primary !rounded-xl" />
|
||||
<div class="flex gap-2">
|
||||
@foreach (var prefix in new[] { "!", "?", ".", "$", "/" })
|
||||
{
|
||||
var p = prefix;
|
||||
<button type="button"
|
||||
@onclick="@(() => _settingsViewModel.Prefix = p)"
|
||||
class="w-10 h-10 rounded-lg font-mono text-lg transition-all duration-200 @(_settingsViewModel.Prefix == p ? "bg-gradient-to-r from-primary to-accent text-white shadow-lg shadow-primary/30" : "bg-dark-tertiary text-slate-300 border border-slate-600 hover:border-primary hover:text-white")">
|
||||
@p
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-slate-500 text-xs flex items-center gap-1">
|
||||
Example: <code class="bg-dark-tertiary px-2 py-0.5 rounded text-primary-light font-mono">@(_settingsViewModel.Prefix)help</code>
|
||||
</p>
|
||||
<RadzenRequiredValidator Component="Prefix" Text="Prefix is required" class="!text-red-400 !text-xs" />
|
||||
</div>
|
||||
|
||||
<!-- Server IDs Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-slate-300">Server IDs</label>
|
||||
<RadzenTextArea @bind-Value="@_settingsViewModel.ServerIds"
|
||||
Name="ServerIds"
|
||||
Placeholder="Enter server IDs separated by commas (e.g., 123456789, 987654321)"
|
||||
Rows="3"
|
||||
class="!w-full !bg-dark-tertiary !border-slate-600 hover:!border-primary focus-within:!border-primary focus-within:!ring-2 focus-within:!ring-primary/20 !rounded-xl !transition-all !duration-300" />
|
||||
<p class="text-slate-500 text-xs flex items-center gap-1">
|
||||
<RadzenIcon Icon="info" class="text-xs" /> Comma-separated list of Discord server IDs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<RadzenButton Text="Save Settings"
|
||||
Icon="save"
|
||||
ButtonType="ButtonType.Submit"
|
||||
ButtonStyle="ButtonStyle.Primary"
|
||||
class="!w-full !bg-gradient-to-r !from-primary !to-accent hover:!shadow-lg hover:!shadow-primary/30 !transition-all !duration-300 hover:!-translate-y-0.5 !rounded-xl !py-3 !font-semibold" />
|
||||
</div>
|
||||
</RadzenTemplateForm>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="bg-blue-500/10 border border-blue-500/20 rounded-2xl p-5 backdrop-blur-sm">
|
||||
<div class="flex gap-4">
|
||||
<div class="p-2 rounded-lg bg-blue-500/20 h-fit">
|
||||
<RadzenIcon Icon="help" class="text-blue-400" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<h4 class="text-sm font-semibold text-blue-300">Need help?</h4>
|
||||
<p class="text-sm text-blue-200/70">
|
||||
You can find your bot token in the
|
||||
<a href="https://discord.com/developers/applications" target="_blank" class="text-blue-400 hover:text-blue-300 underline underline-offset-2">Discord Developer Portal</a>.
|
||||
To get a server ID, enable Developer Mode in Discord settings, then right-click your server.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<ModernLabel Text="Prefix"/>
|
||||
<RoundedTextBox @bind-Value="_settingsViewModel.Prefix" Placeholder="Your bot prefix"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<ModernLabel Text="Server IDs (command-separated)"/>
|
||||
<RoundedTextBox @bind-Value="_settingsViewModel.ServerIds" Placeholder="1234,5678" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
Save
|
||||
</button>
|
||||
</EditForm>
|
||||
</CenteredCard>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public ILogger Logger { get; set; }
|
||||
[Inject] public IConfiguration Configuration { get; set; }
|
||||
[Inject] public ILogger Logger { get; set; } = default!;
|
||||
[Inject] public IConfiguration Configuration { get; set; } = default!;
|
||||
|
||||
private SettingsViewModel? _settingsViewModel;
|
||||
|
||||
@@ -68,13 +137,13 @@
|
||||
if (_settingsViewModel is null) return;
|
||||
|
||||
var ids = _settingsViewModel.ServerIds
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(id => ulong.TryParse(id.Trim(), out var v) ? v : 0)
|
||||
.Where(v => v != 0)
|
||||
.ToList();
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(id => ulong.TryParse(id.Trim(), out var v) ? v : 0)
|
||||
.Where(v => v != 0)
|
||||
.ToList();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_settingsViewModel.Token) ||
|
||||
string.IsNullOrWhiteSpace(_settingsViewModel.Prefix))
|
||||
string.IsNullOrWhiteSpace(_settingsViewModel.Prefix))
|
||||
return;
|
||||
|
||||
Configuration.Set("token", _settingsViewModel.Token);
|
||||
@@ -83,6 +152,7 @@
|
||||
|
||||
await Configuration.SaveToFile();
|
||||
Logger.Log("Settings saved successfully.", this);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Settings saved successfully!");
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
|
||||
@@ -94,7 +164,6 @@
|
||||
[Required(ErrorMessage = "Prefix is required.")]
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Server IDs are required.")]
|
||||
public string ServerIds { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user