Updated WebUI with Radzen and tailwind css

This commit is contained in:
2026-01-26 00:19:59 +02:00
parent bc86195b5a
commit 4653e6eb03
24 changed files with 1586 additions and 722 deletions
+158 -51
View File
@@ -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;
}
}
+153 -55
View File
@@ -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; }
}
+105 -72
View File
@@ -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;
}
}