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
+89 -6
View File
@@ -1,22 +1,105 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="dark rz-dark">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/> <base href="/"/>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <!-- Tailwind CSS CDN -->
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="app.css"/> <script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#8b5cf6',
dark: '#7c3aed',
light: '#a78bfa',
},
accent: '#d946ef',
discord: '#5865F2',
dark: {
primary: '#0f172a',
secondary: '#1e293b',
tertiary: '#334155',
}
},
fontFamily: {
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
},
animation: {
'float': 'float 3s ease-in-out infinite',
'fade-in': 'fadeIn 0.15s ease-out',
'scale-in': 'scaleIn 0.15s ease-out',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-4px)' },
},
fadeIn: {
from: { opacity: '0' },
to: { opacity: '1' },
},
scaleIn: {
from: { opacity: '0', transform: 'scale(0.98)' },
to: { opacity: '1', transform: 'scale(1)' },
},
},
}
}
}
</script>
<!-- Radzen Blazor CSS - Dark theme -->
<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-dark-base.css">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Site CSS -->
<link rel="stylesheet" href="css/site.css"/>
<link rel="stylesheet" href="WebUI.styles.css"/> <link rel="stylesheet" href="WebUI.styles.css"/>
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png"/>
<HeadOutlet/> <HeadOutlet/>
</head> </head>
<body> <body class="rz-background-color font-sans antialiased min-h-screen rz-dark">
<Routes/> <Routes/>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
<script>
// Setup wizard helpers
window.setupWizard = {
isFirstTimeSetup: function() {
return localStorage.getItem('setupComplete') !== 'true';
},
completeSetup: function() {
localStorage.setItem('setupComplete', 'true');
},
resetSetup: function() {
localStorage.removeItem('setupComplete');
}
};
// Scroll helper
window.scrollToBottom = function(elementId) {
var el = document.getElementById(elementId);
if (el) {
el.scrollTop = el.scrollHeight;
}
};
</script>
</body> </body>
</html> </html>
+55 -17
View File
@@ -1,21 +1,59 @@
@using WebUI.Components.Notification @inherits LayoutComponentBase
@inherits LayoutComponentBase
<NotificationDisplay /> <RadzenLayout>
<div class="page"> <RadzenHeader>
<div class="sidebar"> <MobileHeader />
<NavMenu/> </RadzenHeader>
</div>
<RadzenSidebar @bind-Expanded="@_sidebarExpanded" class="!bg-gradient-to-b !from-indigo-950 !via-indigo-900 !to-purple-950 !border-0 shadow-2xl shadow-purple-900/20">
<main> <div class="flex flex-col h-full">
<article class="content px-4"> <!-- Logo Section -->
<div class="p-5 border-b border-white/10">
<div class="flex items-center gap-3.5">
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/40 transition-transform hover:scale-105">
<i class="bi bi-robot text-white text-xl"></i>
</div>
<div>
<h1 class="text-white font-bold text-lg m-0 tracking-tight">Seth Bot</h1>
<p class="text-white/60 text-xs m-0">Discord Bot Manager</p>
</div>
</div>
</div>
<!-- Navigation -->
<div class="flex-1 py-4 px-2 overflow-y-auto scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
<NavMenu />
</div>
<!-- Footer -->
<div class="p-4 border-t border-white/10 mt-auto">
<SidebarFooter />
</div>
</div>
</RadzenSidebar>
<RadzenBody class="!bg-dark-primary">
<div class="p-6 md:p-8 min-h-screen">
@Body @Body
</article> </div>
</main> </RadzenBody>
</RadzenLayout>
<!-- Setup Wizard -->
<SetupWizardWrapper />
<!-- Radzen Components -->
<RadzenDialog />
<RadzenNotification />
<RadzenTooltip />
<RadzenContextMenu />
<div id="blazor-error-ui" class="fixed bottom-0 left-0 w-full bg-amber-500 text-black p-4 z-[9999] text-center hidden">
An unhandled error has occurred.
<a href="" class="reload underline ml-2">Reload</a>
<a class="dismiss absolute right-4 top-1/2 -translate-y-1/2 cursor-pointer">🗙</a>
</div> </div>
<div id="blazor-error-ui"> @code {
An unhandled error has occurred. private bool _sidebarExpanded = true;
<a href="" class="reload">Reload</a> }
<a class="dismiss">🗙</a>
</div>
@@ -1,96 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -0,0 +1,71 @@
@rendermode InteractiveServer
<div class="lg:hidden block p-3 bg-dark-secondary border-b border-slate-700/50">
<div class="flex items-center justify-between">
<button @onclick="ToggleMobileSidebar"
class="p-2 rounded-lg text-slate-300 hover:text-white hover:bg-white/10 transition-colors">
<RadzenIcon Icon="menu" />
</button>
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-md shadow-primary/30">
<i class="bi bi-robot text-white text-sm"></i>
</div>
<span class="font-semibold text-white">Seth Bot</span>
</div>
<SidebarFooter Compact="true" />
</div>
</div>
<!-- Mobile Sidebar Drawer -->
@if (_sidebarOpen)
{
<div class="fixed inset-0 z-50 lg:hidden">
<!-- Overlay -->
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" @onclick="ToggleMobileSidebar"></div>
<!-- Sidebar -->
<div class="fixed left-0 top-0 h-full w-72 bg-gradient-to-b from-indigo-950 via-indigo-900 to-purple-950 shadow-2xl z-10">
<div class="flex flex-col h-full">
<!-- Logo Section -->
<div class="p-5 border-b border-white/10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/40">
<i class="bi bi-robot text-white"></i>
</div>
<div>
<h1 class="text-white font-bold text-base m-0">Seth Bot</h1>
<p class="text-white/60 text-xs m-0">Discord Bot Manager</p>
</div>
</div>
<button @onclick="ToggleMobileSidebar"
class="p-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors">
<RadzenIcon Icon="close" />
</button>
</div>
</div>
<!-- Navigation -->
<div class="flex-1 py-4 px-2 overflow-y-auto">
<NavMenu />
</div>
<!-- Footer with Theme Toggle -->
<div class="p-4 border-t border-white/10 mt-auto">
<SidebarFooter />
</div>
</div>
</div>
</div>
}
@code {
private bool _sidebarOpen;
private void ToggleMobileSidebar()
{
_sidebarOpen = !_sidebarOpen;
}
}
+9 -42
View File
@@ -1,44 +1,11 @@
<div class="top-row ps-3 navbar navbar-dark"> @rendermode InteractiveServer
<div class="container-fluid align-items-center">
<a class="navbar-brand align-content-center" href="">Seth Discord Bot</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler"/> <RadzenPanelMenu>
<RadzenPanelMenuItem Text="Home" Icon="home" Path="" />
<RadzenPanelMenuItem Text="Settings" Icon="settings" Path="settings" />
<RadzenPanelMenuItem Text="Plugins" Icon="extension">
<RadzenPanelMenuItem Text="Online Plugins" Icon="cloud_download" Path="plugins/online" />
<RadzenPanelMenuItem Text="Local Plugins" Icon="folder" Path="plugins/local" />
</RadzenPanelMenuItem>
</RadzenPanelMenu>
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="settings">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Settings
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link d-flex align-items-center" href="plugins/online">
<span class="bi bi-list" aria-hidden="true" style="
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
margin-right: 0.5rem;
font-size: 1.2rem;
"></span>
Online Plugins
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="plugins/local">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Local Plugins
</NavLink>
</div>
</nav>
</div>
-106
View File
@@ -1,106 +0,0 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
+286
View File
@@ -0,0 +1,286 @@
@using DiscordBotCore.Configuration
@using DiscordBotCore.Logging
@inject IConfiguration Configuration
@inject ILogger Logger
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-br from-indigo-950 via-indigo-900 to-purple-950 overflow-hidden">
<!-- Static background blobs (no animation) -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-[10%] left-[10%] w-[500px] h-[500px] rounded-full bg-primary/40 blur-[100px] opacity-60"></div>
<div class="absolute bottom-[10%] right-[10%] w-[600px] h-[600px] rounded-full bg-pink-500/30 blur-[100px] opacity-60"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[400px] rounded-full bg-blue-500/25 blur-[100px] opacity-60"></div>
</div>
<!-- Setup Card -->
<div class="relative w-full max-w-[480px] mx-4 bg-white/[0.08] backdrop-blur-2xl rounded-3xl border border-white/[0.12] overflow-hidden">
<!-- Progress Bar -->
<div class="h-1 bg-white/10">
<div class="h-full bg-gradient-to-r from-primary to-accent transition-all duration-300 ease-out" style="width: @(_currentStep * 100 / TotalSteps)%"></div>
</div>
<!-- Content -->
<div class="p-8">
@switch (_currentStep)
{
case 1:
<!-- Welcome Step -->
<div class="flex flex-col items-center gap-6">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-2xl shadow-primary/40">
<i class="bi bi-robot text-white text-3xl"></i>
</div>
<div class="text-center space-y-2">
<h2 class="text-2xl font-bold text-white">Welcome to Seth Bot!</h2>
<p class="text-white/70">Let's get your Discord bot up and running in just a few steps.</p>
</div>
<div class="flex gap-4 mt-2">
<div class="p-4 rounded-xl bg-white/[0.08] border border-white/10 text-center min-w-[100px]">
<RadzenIcon Icon="bolt" class="text-amber-400 text-2xl" />
<p class="text-white text-xs mt-2 font-medium">Fast Setup</p>
</div>
<div class="p-4 rounded-xl bg-white/[0.08] border border-white/10 text-center min-w-[100px]">
<RadzenIcon Icon="extension" class="text-emerald-400 text-2xl" />
<p class="text-white text-xs mt-2 font-medium">Plugins</p>
</div>
<div class="p-4 rounded-xl bg-white/[0.08] border border-white/10 text-center min-w-[100px]">
<RadzenIcon Icon="security" class="text-blue-400 text-2xl" />
<p class="text-white text-xs mt-2 font-medium">Secure</p>
</div>
</div>
</div>
break;
case 2:
<!-- Token Step -->
<div class="flex flex-col items-center gap-6">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary to-purple-500 flex items-center justify-center shadow-2xl shadow-primary/40">
<RadzenIcon Icon="vpn_key" class="text-white text-3xl" />
</div>
<div class="text-center space-y-2">
<h2 class="text-xl font-bold text-white">Bot Token</h2>
<p class="text-white/70">Enter your Discord bot token to continue</p>
</div>
<div class="w-full space-y-4">
<RadzenPassword @bind-Value="@_token"
Placeholder="Paste your bot token here..."
class="!w-full !bg-white/[0.08] !border-white/15 hover:!border-primary focus-within:!border-primary !text-white !rounded-xl !placeholder:text-white/40" />
<div class="p-4 rounded-xl bg-blue-500/10 border border-blue-500/20">
<p class="text-white text-sm">
<span class="font-semibold">Where to find your token?</span><br/>
<span class="text-white/70">Go to Discord Developer Portal → Your Application → Bot → Token</span>
</p>
</div>
</div>
</div>
break;
case 3:
<!-- Prefix Step -->
<div class="flex flex-col items-center gap-6">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-2xl shadow-emerald-500/40">
<RadzenIcon Icon="chat" class="text-white text-3xl" />
</div>
<div class="text-center space-y-2">
<h2 class="text-xl font-bold text-white">Command Prefix</h2>
<p class="text-white/70">Choose a prefix for your bot commands</p>
</div>
<div class="w-full space-y-4 flex flex-col items-center">
<RadzenTextBox @bind-Value="@_prefix"
Placeholder="!"
MaxLength="5"
class="!w-24 !text-center !text-2xl !font-mono !bg-white/[0.08] !border-white/15 hover:!border-primary focus-within:!border-primary !text-white !rounded-xl" />
<div class="flex gap-2 justify-center">
@foreach (var prefix in new[] { "!", "?", ".", "$", "/" })
{
var p = prefix;
<button type="button"
@onclick="@(() => _prefix = p)"
class="w-12 h-12 rounded-xl font-mono text-xl transition-colors @(_prefix == p ? "bg-gradient-to-r from-primary to-accent text-white shadow-lg shadow-primary/30" : "bg-white/[0.08] text-white/70 border border-white/15 hover:border-primary hover:text-white")">
@p
</button>
}
</div>
<p class="text-white/60 text-sm">
Example: <code class="bg-white/10 px-2 py-0.5 rounded text-white font-mono">@(_prefix)help</code>
</p>
</div>
</div>
break;
case 4:
<!-- Server IDs Step -->
<div class="flex flex-col items-center gap-6">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-orange-500 to-amber-500 flex items-center justify-center shadow-2xl shadow-orange-500/40">
<RadzenIcon Icon="dns" class="text-white text-3xl" />
</div>
<div class="text-center space-y-2">
<h2 class="text-xl font-bold text-white">Server IDs</h2>
<p class="text-white/70">Add the Discord server IDs where your bot will operate</p>
</div>
<div class="w-full space-y-4">
<RadzenTextArea @bind-Value="@_serverIds"
Placeholder="Enter server IDs separated by commas..."
Rows="3"
class="!w-full !bg-white/[0.08] !border-white/15 hover:!border-primary focus-within:!border-primary !text-white !rounded-xl !placeholder:text-white/40" />
<div class="p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<p class="text-white text-sm">
<span class="font-semibold">How to get Server ID?</span><br/>
<span class="text-white/70">Enable Developer Mode in Discord Settings → Right-click your server → Copy Server ID</span>
</p>
</div>
<p class="text-white/50 text-xs text-center">Optional: You can add this later in Settings</p>
</div>
</div>
break;
case 5:
<!-- Complete Step -->
<div class="flex flex-col items-center gap-6">
<div class="w-20 h-20 rounded-full bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-2xl shadow-emerald-500/40">
<RadzenIcon Icon="check" class="text-white text-4xl" />
</div>
<div class="text-center space-y-2">
<h2 class="text-2xl font-bold text-white">You're All Set!</h2>
<p class="text-white/70">Your bot is configured and ready to go.</p>
</div>
<div class="w-full p-5 rounded-xl bg-white/[0.08] border border-white/10">
<div class="flex items-center gap-2 mb-4 text-white">
<RadzenIcon Icon="list" class="text-white/70" />
<span class="font-semibold">Configuration Summary</span>
</div>
<div class="space-y-3 text-sm">
<div class="flex justify-between items-center">
<span class="text-white/60">Token</span>
<code class="text-white bg-white/10 px-2 py-0.5 rounded font-mono">••••••••@(_token.Length > 8 ? _token.Substring(_token.Length - 4) : "••••")</code>
</div>
<div class="flex justify-between items-center">
<span class="text-white/60">Prefix</span>
<code class="text-white bg-white/10 px-2 py-0.5 rounded font-mono">@_prefix</code>
</div>
<div class="flex justify-between items-center">
<span class="text-white/60">Server IDs</span>
<span class="text-white">@(string.IsNullOrEmpty(_serverIds) ? "None specified" : _serverIds.Split(',').Length + " server(s)")</span>
</div>
</div>
</div>
<p class="text-white/50 text-xs">You can change these settings anytime</p>
</div>
break;
}
<!-- Navigation Buttons -->
<div class="flex justify-between mt-8 gap-4">
@if (_currentStep > 1 && _currentStep < TotalSteps)
{
<button @onclick="PreviousStep"
class="px-5 py-2.5 rounded-xl border border-white/30 text-white hover:bg-white/10 transition-all duration-200 flex items-center gap-2">
<RadzenIcon Icon="arrow_back" class="text-sm" />
Back
</button>
}
else
{
<div></div>
}
@if (_currentStep < TotalSteps)
{
<RadzenButton Text="@(_currentStep == 1 ? "Get Started" : "Continue")"
Icon="arrow_forward"
IconPosition="ButtonIconPosition.Right"
ButtonStyle="ButtonStyle.Primary"
Disabled="@(!CanProceed())"
Click="@NextStep"
class="!bg-gradient-to-r !from-primary !to-accent hover:!shadow-lg hover:!shadow-primary/30 !transition-all !duration-300 !rounded-xl" />
}
else
{
<RadzenButton Text="Launch Bot"
Icon="rocket_launch"
ButtonStyle="ButtonStyle.Success"
Click="@FinishSetup"
class="!bg-gradient-to-r !from-emerald-500 !to-teal-500 !rounded-xl" />
}
</div>
<!-- Step Indicators -->
<div class="flex justify-center gap-2 mt-6">
@for (int i = 1; i <= TotalSteps; i++)
{
var step = i;
<div class="w-2 h-2 rounded-full transition-all duration-300 @(step <= _currentStep ? "bg-white scale-125" : "bg-white/25")"></div>
}
</div>
</div>
</div>
</div>
@code {
[Parameter] public EventCallback OnSetupComplete { get; set; }
private int _currentStep = 1;
private const int TotalSteps = 5;
private string _token = string.Empty;
private string _prefix = "!";
private string _serverIds = string.Empty;
private void NextStep()
{
if (_currentStep < TotalSteps && CanProceed())
{
_currentStep++;
}
}
private void PreviousStep()
{
if (_currentStep > 1)
{
_currentStep--;
}
}
private bool CanProceed()
{
return _currentStep switch
{
1 => true,
2 => !string.IsNullOrWhiteSpace(_token),
3 => !string.IsNullOrWhiteSpace(_prefix),
4 => true,
_ => true
};
}
private async Task FinishSetup()
{
// Save configuration
Configuration.Set("token", _token);
Configuration.Set("prefix", _prefix);
var serverIdList = _serverIds
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(id => ulong.TryParse(id.Trim(), out var v) ? v : 0)
.Where(v => v != 0)
.ToList();
Configuration.Set("ServerIds", serverIdList);
await Configuration.SaveToFile();
Logger.Log("Initial setup completed successfully!", this);
await OnSetupComplete.InvokeAsync();
}
}
@@ -0,0 +1,31 @@
@using DiscordBotCore.Configuration
@inject IJSRuntime JS
@inject IConfiguration Configuration
@rendermode InteractiveServer
@if (_showSetupWizard)
{
<SetupWizard OnSetupComplete="HandleSetupComplete" />
}
@code {
private bool _showSetupWizard;
protected override void OnInitialized()
{
// Check if token is configured - if not, show setup wizard
var token = Configuration.Get<string>("token");
if (string.IsNullOrWhiteSpace(token))
{
_showSetupWizard = true;
}
}
private async Task HandleSetupComplete()
{
await JS.InvokeVoidAsync("setupWizard.completeSetup");
_showSetupWizard = false;
StateHasChanged();
}
}
@@ -0,0 +1,21 @@
@if (Compact)
{
<div class="p-2 rounded-lg text-white/80">
<RadzenIcon Icon="dark_mode" />
</div>
}
else
{
<div class="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/10">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg flex items-center justify-center bg-gradient-to-br from-indigo-500 to-primary">
<RadzenIcon Icon="dark_mode" class="text-white text-sm" />
</div>
<span class="text-white/90 text-sm font-medium">Copyright @DateTime.Now.Year</span>
</div>
</div>
}
@code {
[Parameter] public bool Compact { get; set; }
}
@@ -1,56 +0,0 @@
@using WebUI.Models
@using WebUI.Services
@inject NotificationService NotificationService
@rendermode InteractiveServer
<link rel="stylesheet" href="Components/Notification/NotificationDisplay.css" />
<div class="notification-container" style="position: fixed; top: 20px; right: 20px; z-index: 1050; display: flex; flex-direction: column; gap: 10px;">
@foreach (var notification in _Notifications)
{
<div class="notification-box @GetCssClass(notification.Type)">
<button class="close-button" @onclick="() => DismissNotification(notification)">×</button>
<div class="notification-message">@notification.Message</div>
<div class="notification-timestamp">@notification.Timestamp.ToString("hh:mm:ss tt")</div>
</div>
}
</div>
@code {
private readonly List<Notification> _Notifications = new();
protected override void OnInitialized()
{
NotificationService.OnNotify += ShowNotification;
}
private void ShowNotification(Notification notification)
{
_ = InvokeAsync(async () =>
{
_Notifications.Add(notification);
StateHasChanged();
await Task.Delay(notification.DelayMs);
_Notifications.Remove(notification);
StateHasChanged();
});
}
private string GetCssClass(NotificationType type) => type switch
{
NotificationType.Success => "alert-success",
NotificationType.Error=> "alert-danger",
NotificationType.Warning => "alert-warning",
NotificationType.Info => "alert-info",
_ => "alert-secondary"
};
private void DismissNotification(Notification notification)
{
_Notifications.Remove(notification);
}
}
+39 -21
View File
@@ -1,31 +1,49 @@
@page "/Error" @page "/Error"
@using System.Diagnostics @using System.Diagnostics
<PageTitle>Error</PageTitle> <PageTitle>Error - Seth Bot</PageTitle>
<h1 class="text-danger">Error.</h1> <RadzenStack AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Gap="1.5rem" class="animate-fade-in" Style="min-height: 70vh;">
<h2 class="text-danger">An error occurred while processing your request.</h2> <div class="sidebar-logo-icon" style="width: 80px; height: 80px; background: linear-gradient(135deg, #ef4444, #f43f5e);">
<RadzenIcon Icon="error" Style="font-size: 2.5rem;" />
@if (ShowRequestId) </div>
{
<p> <RadzenText TextStyle="TextStyle.H3" TextAlign="TextAlign.Center"><strong>Oops!</strong></RadzenText>
<strong>Request ID:</strong> <code>@RequestId</code> <RadzenText TextStyle="TextStyle.Subtitle1" TextAlign="TextAlign.Center" class="rz-color-secondary">An error occurred while processing your request.</RadzenText>
</p>
} @if (ShowRequestId)
{
<h3>Development Mode</h3> <RadzenCard class="card-modern" Style="padding: 1rem;">
<p> <RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary">Request ID</RadzenText>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred. <code>@RequestId</code>
</p> </RadzenCard>
<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. <RadzenCard class="card-modern" Style="max-width: 500px;">
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong> <RadzenStack Gap="0.75rem">
and restarting the app. <RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0.5rem">
</p> <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{ @code{
[CascadingParameter] private HttpContext? HttpContext { get; set; } [CascadingParameter] private HttpContext? HttpContext { get; set; }
[Inject] private NavigationManager Navigation { get; set; } = default!;
private string? RequestId { get; set; } private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+139 -62
View File
@@ -2,8 +2,7 @@
@using DiscordBotCore.Bot @using DiscordBotCore.Bot
@using DiscordBotCore.Logging @using DiscordBotCore.Logging
@using DiscordBotCore.PluginManagement.Loading @using DiscordBotCore.PluginManagement.Loading
@using WebUI.Models @implements IDisposable
@using WebUI.Services
@inject IDiscordBotApplication DiscordBotApplication @inject IDiscordBotApplication DiscordBotApplication
@inject IPluginLoader PluginLoader @inject IPluginLoader PluginLoader
@@ -13,73 +12,132 @@
@rendermode InteractiveServer @rendermode InteractiveServer
<link rel="stylesheet" href="Components/Pages/Home.css" /> <PageTitle>Home - Seth Bot</PageTitle>
<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>
<button class="btn btn-danger" <div class="space-y-6">
@onclick="StopApplication" <!-- Header Section -->
disabled="@(!IsRunning)"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<i class="bi bi-stop-fill me-1"></i> Stop Application <div>
</button> <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>
<button class="btn btn-warning text-dark" </div>
@onclick="LoadPlugins"
disabled="@(!IsRunning)"> <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")">
<i class="bi bi-plug-fill me-1"></i> Load Plugins <span class="w-2 h-2 rounded-full @(IsRunning ? "bg-emerald-400" : "bg-red-400")"></span>
</button> @(IsRunning ? "Online" : "Offline")
</div>
<button class="btn btn-secondary"
@onclick="ClearLogs">
<i class="bi bi-trash3-fill me-1"></i> Clear Logs
</button>
</div> </div>
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center"> <!-- Control Panel -->
<h5 class="mb-0">Console Log</h5> <div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-6">
<span class="badge bg-secondary">@Logger.LogMessages.Count()</span> <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>
<div id="consoleLog"
class="bg-dark text-white p-3 flex-grow-1 overflow-auto" <!-- Console Log -->
style="font-family:monospace;"> <div class="console-container bg-[#0d1117] border border-[#21262d] rounded-2xl overflow-hidden">
@foreach (var line in Logger.LogMessages) <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 style="@GetLogStyle(line)">@line.Message</div> <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>
</div> </div>
<script>
window.scrollToBottom = function (elementId) {
var el = document.getElementById(elementId);
if (el) {
el.scrollTop = el.scrollHeight;
}
}
</script>
@code { @code {
private bool IsRunning { get; set; } private bool IsRunning { get; set; }
private int _logCount;
private System.Timers.Timer? _debounceTimer;
private bool _pendingUpdate;
protected override void OnInitialized() protected override void OnInitialized()
{ {
IsRunning = DiscordBotApplication.IsReady; IsRunning = DiscordBotApplication.IsReady;
_logCount = Logger.LogMessages.Count();
Logger.OnLogReceived += LoggerOnLogReceived; 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) private void LoggerOnLogReceived(ILogMessage obj)
{ {
InvokeAsync(async () => _pendingUpdate = true;
{
await JS.InvokeVoidAsync("scrollToBottom", "consoleLog");
StateHasChanged();
});
} }
private async Task StartApplication() private async Task StartApplication()
@@ -88,7 +146,7 @@
{ {
await DiscordBotApplication.StartAsync(); await DiscordBotApplication.StartAsync();
Logger.Log("Application started", this); Logger.Log("Application started", this);
NotificationService.Notify("Bot Started !", NotificationType.Success); NotificationService.Notify(NotificationSeverity.Success, "Success", "Bot Started!");
} }
IsRunning = DiscordBotApplication.IsReady; IsRunning = DiscordBotApplication.IsReady;
@@ -100,7 +158,7 @@
{ {
await DiscordBotApplication.StopAsync(); await DiscordBotApplication.StopAsync();
Logger.Log("Application stopped", this); Logger.Log("Application stopped", this);
NotificationService.Notify("Bot Stopped !", NotificationType.Success); NotificationService.Notify(NotificationSeverity.Success, "Success", "Bot Stopped!");
} }
IsRunning = DiscordBotApplication.IsReady; IsRunning = DiscordBotApplication.IsReady;
@@ -111,24 +169,43 @@
Logger.Log("Loading plugins", this); Logger.Log("Loading plugins", this);
await PluginLoader.LoadPlugins(); await PluginLoader.LoadPlugins();
Logger.Log("Plugins loaded", this); 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 return logMessage.LogMessageType switch
{ {
LogType.Info => "color: white;", LogType.Info => "text-sky-400",
LogType.Warning => "color: yellow;", LogType.Warning => "text-amber-400",
LogType.Error => "color: red;", LogType.Error => "text-red-400",
LogType.Critical => "color: purple;", 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() private void ClearLogs()
{ {
Logger.LogMessages.Clear(); Logger.LogMessages.Clear();
_logCount = 0;
}
public void Dispose()
{
Logger.OnLogReceived -= LoggerOnLogReceived;
_debounceTimer?.Stop();
_debounceTimer?.Dispose();
} }
} }
+158 -51
View File
@@ -5,74 +5,181 @@
@using DiscordBotCore.Logging @using DiscordBotCore.Logging
@using DiscordBotCore.PluginManagement @using DiscordBotCore.PluginManagement
@using DiscordBotCore.PluginManagement.Models @using DiscordBotCore.PluginManagement.Models
@using WebUI.Models
@using WebUI.Services
@using WebUI.Components.Shared
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject NotificationService NotificationService @inject NotificationService NotificationService
@inject IPluginManager PluginManager @inject IPluginManager PluginManager
@inject IConfiguration Configuration @inject IConfiguration Configuration
@inject ILogger Logger @inject ILogger Logger
@inject IJSRuntime JS
@rendermode InteractiveServer @rendermode InteractiveServer
<CenteredCard Title="Upload plugin"> <PageTitle>Add Local Plugin - Seth Bot</PageTitle>
<EditForm Model="@pluginModel" OnValidSubmit="HandleValidSubmit" FormName="pluginForm">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group"> <div class="max-w-xl mx-auto space-y-6">
<ModernLabel Text="Plugin Name"></ModernLabel> <!-- Header -->
<RoundedTextBox @bind-Value="pluginModel.Name" Placeholder="Example Plugin"></RoundedTextBox> <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>
<div>
<div class="form-group"> <h2 class="text-2xl font-bold text-white">Add Local Plugin</h2>
<ModernLabel Text="Plugin version"></ModernLabel> <p class="text-slate-400 text-sm">Upload a plugin file from your computer</p>
<RoundedTextBox @bind-Value="pluginModel.Version" Placeholder="1.0.0"></RoundedTextBox>
</div> </div>
</div>
<div class="form-group"> <div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-6">
<ModernCheckbox @bind-Checked="pluginModel.IsEnabled" Label="Enable Plugin"></ModernCheckbox> <RadzenTemplateForm TItem="PluginUploadModel" Data="@_pluginModel" Submit="@HandleValidSubmit">
</div> <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"> <!-- Plugin Version -->
<ModernFileUploader OnFileUploaded="HandleUploadedFile" AllowedFileTypes="@allowedFileTypes"/> <div class="space-y-2">
@if (_message is not null) <label class="block text-sm font-medium text-slate-300">Version</label>
{ <RadzenTextBox @bind-Value="@_pluginModel.Version"
<p class="mt-2 text-green-600">@_message</p> 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> <!-- File Upload -->
</EditForm> <div class="space-y-2">
</CenteredCard> <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 { @code {
private PluginUploadModel pluginModel = new(); private PluginUploadModel _pluginModel = new();
private IBrowserFile? selectedFile; private IBrowserFile? _selectedFile;
private string? _uploadMessage;
private List<string> allowedFileTypes { get; set; } = new List<string> { ".dll"}; 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 JS.InvokeVoidAsync("eval", "document.getElementById('pluginFileInput').click()");
await file.OpenReadStream().ReadAsync(buffer); }
_message = $"Uploaded file: {file.Name} ({file.Size} bytes)";
selectedFile = file; 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() 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; return;
} }
@@ -81,32 +188,32 @@
if (string.IsNullOrEmpty(pluginsFolder)) if (string.IsNullOrEmpty(pluginsFolder))
{ {
Logger.Log("Plugins folder is not set in the configuration.", this, LogType.Error); 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; 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 using var fileStream = File.Create(filePath);
await stream.CopyToAsync(fileStream); 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); 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; public string Name { get; set; } = string.Empty;
[Required] [Required(ErrorMessage = "Version is required.")]
public string Version { get; set; } = string.Empty; 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 IPluginManager PluginManager
@inject ILogger Logger @inject ILogger Logger
@inject IPluginLoader PluginLoader @inject IPluginLoader PluginLoader
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject NotificationService NotificationService
@inject DialogService DialogService
<h3>Installed Plugins</h3> <PageTitle>Local Plugins - Seth Bot</PageTitle>
<button class="btn btn-outline-success" @onclick="NavigateToAddPlugin">
<span class="bi me-1"></span> Add Local <div class="space-y-6">
</button> <!-- Header -->
<table class="table table-responsive"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<thead> <div class="flex items-center gap-4">
<tr> <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">
<th>Name</th> <RadzenIcon Icon="folder" class="text-white" />
<th>Version</th> </div>
<th>Offline Added</th> <div>
<th>Actions</th> <h2 class="text-2xl font-bold text-white">Local Plugins</h2>
@* <th> *@ <p class="text-slate-400 text-sm">Manage your installed plugins</p>
@* <button class="btn btn-outline-success" @onclick="NavigateToAddPlugin"> *@ </div>
@* <span class="bi me-1"></span> Add Local *@ </div>
@* </button> *@
@* </th> *@ <RadzenButton Text="Add Local Plugin"
</tr> Icon="add"
</thead> ButtonStyle="ButtonStyle.Success"
<tbody> Click="@NavigateToAddPlugin"
@foreach (var plugin in _installedPlugins) class="!bg-gradient-to-r !from-emerald-500 !to-teal-500 !rounded-xl" />
</div>
@if (_installedPlugins.Any())
{ {
<tr> <!-- Plugins DataGrid -->
<td>@plugin.Name</td> <div class="bg-dark-secondary border border-slate-700/50 rounded-2xl overflow-hidden shadow-xl">
<td>@plugin.Version</td> <RadzenDataGrid Data="@_installedPlugins" TItem="InstalledPlugin" AllowSorting="true">
<td>@(plugin.IsOfflineAdded ? "Yes" : "No")</td> <Columns>
<td > <RadzenDataGridColumn TItem="InstalledPlugin" Title="Plugin" Width="auto">
<button class="btn btn-danger btn-sm m-1" @onclick="async () => await DeletePluginButtonClick(plugin)">Delete</button> <Template Context="plugin">
@if (!plugin.IsOfflineAdded) <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">
<button class="btn btn-info btn-sm m-1" @onclick="async () => await PluginDetailsButtonClick(plugin)">Details</button> <RadzenIcon Icon="extension" class="text-white text-sm" />
} </div>
</td> <span class="font-semibold text-white">@plugin.Name</span>
@* <td></td> *@ </div>
</tr> </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> else
</table> {
<!-- 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) @if (_showPluginDetailsModal && _selectedPluginDetails != null)
{ {
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);"> <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
<div class="modal-dialog"> <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">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <div class="bg-gradient-to-r from-pink-500 to-rose-500 p-5">
<h5 class="modal-title">Plugin Details: @_selectedPluginDetails.Name</h5> <div class="flex items-center justify-between">
<button type="button" class="btn-close" @onclick="ClosePluginDetailsModal"></button> <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>
<div class="modal-body"> </div>
<p><strong>Version:</strong> @_selectedPluginDetails.Version</p>
<p><strong>Author:</strong> @_selectedPluginDetails.Author</p> <!-- Content -->
<p><strong>Description:</strong> @_selectedPluginDetails.Description</p> <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>
<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>
</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>
</div> </div>
} }
@code { @code {
private readonly List<InstalledPlugin> _installedPlugins = new List<InstalledPlugin>();
private readonly List<InstalledPlugin> _installedPlugins = new();
private bool _showPluginDetailsModal; private bool _showPluginDetailsModal;
private OnlinePlugin? _selectedPluginDetails; private OnlinePlugin? _selectedPluginDetails;
@@ -91,14 +189,15 @@
if (!response.IsSuccess) if (!response.IsSuccess)
{ {
Logger.Log(response.Message, this, LogType.Error); Logger.Log(response.Message, this, LogType.Error);
NotificationService.Notify(NotificationSeverity.Error, "Error", response.Message);
return; return;
} }
_installedPlugins.RemoveAll(p => p.Name == plugin.Name); _installedPlugins.RemoveAll(p => p.Name == plugin.Name);
Logger.Log($"Plugin {plugin.Name} deleted", this); Logger.Log($"Plugin {plugin.Name} deleted", this);
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {plugin.Name} deleted successfully!");
await PluginLoader.LoadPlugins(); await PluginLoader.LoadPlugins();
StateHasChanged(); StateHasChanged();
} }
@@ -121,7 +220,6 @@
private void ClosePluginDetailsModal() private void ClosePluginDetailsModal()
{ {
Logger.Log("Closing plugin details modal", this);
_showPluginDetailsModal = false; _showPluginDetailsModal = false;
_selectedPluginDetails = null; _selectedPluginDetails = null;
StateHasChanged(); StateHasChanged();
@@ -156,8 +254,8 @@
private class InstalledPlugin private class InstalledPlugin
{ {
public string Name { get; set; } public string Name { get; set; } = string.Empty;
public string Version { get; set; } public string Version { get; set; } = string.Empty;
public bool IsOfflineAdded { get; set; } public bool IsOfflineAdded { get; set; }
} }
+105 -72
View File
@@ -4,87 +4,121 @@
@using DiscordBotCore.Logging @using DiscordBotCore.Logging
@using DiscordBotCore.PluginManagement @using DiscordBotCore.PluginManagement
@using WebUI.Models
@using WebUI.Services
@inject NotificationService NotificationService @inject NotificationService NotificationService
@inject DialogService DialogService
<PageTitle>Online Plugins - Seth Bot</PageTitle>
<h3>Available Plugins</h3> <div class="space-y-6">
@if (_onlinePlugins.Any()) <!-- Header -->
{ <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<table class="table table-responsive text-center align-middle"> <div class="flex items-center gap-4">
<thead> <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">
<tr> <RadzenIcon Icon="cloud_download" class="text-white" />
<th>Name</th> </div>
<th>Description</th> <div>
<th>Author</th> <h2 class="text-2xl font-bold text-white">Online Plugins</h2>
<th>Version</th> <p class="text-slate-400 text-sm">Browse and install plugins from the marketplace</p>
<th>Download</th> </div>
</tr> </div>
</thead>
<tbody> <span class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-emerald-500/15 text-emerald-400">
@foreach (var plugin in _onlinePlugins) @_onlinePlugins.Count available
{ </span>
<tr> </div>
<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>
}
@if (_onlinePlugins.Any())
@if (_showInstallPercentage) {
{ <!-- Plugins Grid -->
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0, 0, 0, 0.5);"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="modal-dialog modal-dialog-centered"> @foreach (var plugin in _onlinePlugins)
<div class="modal-content"> {
<div class="modal-header"> <div @key="plugin.Id" class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-5 hover:border-primary/50 transition-colors">
<h5 class="modal-title">Installing Plugin...</h5> <div class="flex flex-col h-full gap-4">
<button type="button" class="btn-close" @onclick="CloseInstallPercentageModal"></button> <div class="flex justify-between items-start">
</div> <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">
<div class="modal-body"> <RadzenIcon Icon="extension" class="text-white" />
<p>Progress: @($"{_installPercentage:F0}")%</p> </div>
<div class="progress"> <span class="px-2.5 py-1 text-xs font-medium rounded-lg bg-slate-700/50 text-slate-300">
<div class="progress-bar progress-bar-striped progress-bar-animated" v@plugin.Version
role="progressbar" </span>
style="width: @_installPercentage%" </div>
aria-valuenow="@_installPercentage"
aria-valuemin="0" <div class="flex-1 space-y-1">
aria-valuemax="100"> <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>
</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>
<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> </div>
</div> </div>
} }
@code { @code {
[Inject] [Inject] public IPluginManager PluginManager { get; set; } = default!;
public IPluginManager PluginManager { get; set; } [Inject] public ILogger Logger { get; set; } = default!;
[Inject]
public ILogger Logger { get; set; }
private bool _showInstallPercentage; private bool _showInstallPercentage;
private float _installPercentage = 0f; private float _installPercentage;
private readonly List<OnlinePluginModel> _onlinePlugins = new(); private readonly List<OnlinePluginModel> _onlinePlugins = new();
@@ -132,7 +166,7 @@ else
if (pluginData is null) if (pluginData is null)
{ {
Logger.Log("Plugin data is null.", this, LogType.Error); 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; return;
} }
@@ -148,8 +182,7 @@ else
await PluginManager.InstallPlugin(pluginData, progress); await PluginManager.InstallPlugin(pluginData, progress);
Logger.Log($"Plugin {pluginData.Name} installed successfully.", this); Logger.Log($"Plugin {pluginData.Name} installed successfully.", this);
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {pluginData.Name} installed successfully!");
NotificationService.Notify($"Plugin {pluginData.Name} installed successfully!", NotificationType.Success);
CloseInstallPercentageModal(); CloseInstallPercentageModal();
StateHasChanged(); StateHasChanged();
@@ -165,9 +198,9 @@ else
private class OnlinePluginModel private class OnlinePluginModel
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; } = string.Empty;
public string Description { get; set; } public string Description { get; set; } = string.Empty;
public string Author { get; set; } public string Author { get; set; } = string.Empty;
public string Version { get; set; } public string Version { get; set; } = string.Empty;
} }
} }
+104 -35
View File
@@ -2,43 +2,112 @@
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@using DiscordBotCore.Configuration @using DiscordBotCore.Configuration
@using DiscordBotCore.Logging @using DiscordBotCore.Logging
@using WebUI.Components.Shared
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject NotificationService NotificationService
@rendermode InteractiveServer @rendermode InteractiveServer
@if (_settingsViewModel is not null) <PageTitle>Settings - Seth Bot</PageTitle>
{
<CenteredCard Title="Settings">
<EditForm Model="_settingsViewModel" OnValidSubmit="HandleSubmitTask">
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" />
<div class="mb-3"> <div class="max-w-xl mx-auto space-y-6">
<ModernLabel Text="Token"/> <!-- Header -->
<RoundedTextBox @bind-Value="_settingsViewModel.Token" Placeholder="Your bot token"/> <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>
</div>
<div class="mb-3"> }
<ModernLabel Text="Prefix"/> </div>
<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>
}
@code { @code {
[Inject] public ILogger Logger { get; set; } [Inject] public ILogger Logger { get; set; } = default!;
[Inject] public IConfiguration Configuration { get; set; } [Inject] public IConfiguration Configuration { get; set; } = default!;
private SettingsViewModel? _settingsViewModel; private SettingsViewModel? _settingsViewModel;
@@ -68,13 +137,13 @@
if (_settingsViewModel is null) return; if (_settingsViewModel is null) return;
var ids = _settingsViewModel.ServerIds var ids = _settingsViewModel.ServerIds
.Split(',', StringSplitOptions.RemoveEmptyEntries) .Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(id => ulong.TryParse(id.Trim(), out var v) ? v : 0) .Select(id => ulong.TryParse(id.Trim(), out var v) ? v : 0)
.Where(v => v != 0) .Where(v => v != 0)
.ToList(); .ToList();
if (string.IsNullOrWhiteSpace(_settingsViewModel.Token) || if (string.IsNullOrWhiteSpace(_settingsViewModel.Token) ||
string.IsNullOrWhiteSpace(_settingsViewModel.Prefix)) string.IsNullOrWhiteSpace(_settingsViewModel.Prefix))
return; return;
Configuration.Set("token", _settingsViewModel.Token); Configuration.Set("token", _settingsViewModel.Token);
@@ -83,6 +152,7 @@
await Configuration.SaveToFile(); await Configuration.SaveToFile();
Logger.Log("Settings saved successfully.", this); Logger.Log("Settings saved successfully.", this);
NotificationService.Notify(NotificationSeverity.Success, "Success", "Settings saved successfully!");
Navigation.NavigateTo("/"); Navigation.NavigateTo("/");
} }
@@ -94,7 +164,6 @@
[Required(ErrorMessage = "Prefix is required.")] [Required(ErrorMessage = "Prefix is required.")]
public string Prefix { get; set; } = string.Empty; public string Prefix { get; set; } = string.Empty;
[Required(ErrorMessage = "Server IDs are required.")]
public string ServerIds { get; set; } = string.Empty; public string ServerIds { get; set; } = string.Empty;
} }
} }
+4 -1
View File
@@ -6,5 +6,8 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Radzen
@using Radzen.Blazor
@using WebUI @using WebUI
@using WebUI.Components @using WebUI.Components
@using WebUI.Components.Layout
-25
View File
@@ -1,25 +0,0 @@
namespace WebUI.Models;
/// <summary>
/// Notification for the Notification System
/// </summary>
public class Notification
{
/// <summary>
/// The message of the notification.
/// </summary>
public string Message { get; set; }
/// <summary>
/// The type of the notification
/// </summary>
public NotificationType Type { get; set; }
/// <summary>
/// The time when the notification was being sent
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.Now;
/// <summary>
/// The time in milliseconds while the notification should be displayed.
/// The notification should disappear after this time
/// </summary>
public int DelayMs { get; set; } = 1500;
}
-12
View File
@@ -1,12 +0,0 @@
namespace WebUI.Models;
/// <summary>
/// The notification type
/// </summary>
public enum NotificationType
{
Success,
Error,
Warning,
Info
}
+4 -2
View File
@@ -1,6 +1,6 @@
using System.Reflection; using System.Reflection;
using Radzen;
using WebUI; using WebUI;
using WebUI.Services;
using WebUI.Components; using WebUI.Components;
#region Load External (Unmanaged) Assemblies #region Load External (Unmanaged) Assemblies
@@ -69,7 +69,9 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveServerComponents();
builder.AddDiscordBotComponents(); builder.AddDiscordBotComponents();
builder.Services.AddSingleton<NotificationService>();
// Add Radzen services
builder.Services.AddRadzenComponents();
var app = builder.Build(); var app = builder.Build();
-12
View File
@@ -1,12 +0,0 @@
using WebUI.Models;
namespace WebUI.Services;
public class NotificationService
{
public event Action<Notification> OnNotify;
public void Notify(string message, NotificationType type = NotificationType.Info)
{
OnNotify?.Invoke(new Notification { Message = message, Type = type });
}
}
+9
View File
@@ -22,4 +22,13 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Radzen.Blazor" Version="8.6.5" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Data\Resources\config.json" />
<_ContentIncludedByDefault Remove="Data\Resources\plugins.json" />
</ItemGroup>
</Project> </Project>
-51
View File
@@ -1,51 +0,0 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
+309
View File
@@ -0,0 +1,309 @@
/* ===================================
Seth Discord Bot - Tailwind + Radzen Integration
Minimal Radzen Overrides Only
=================================== */
/* ===================================
CSS Variables (for Radzen compatibility)
=================================== */
:root {
--primary: #8b5cf6;
--primary-dark: #7c3aed;
--accent: #d946ef;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--border-color: #334155;
}
/* ===================================
Performance Optimizations
=================================== */
* {
/* Disable animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
animation: none !important;
transition: none !important;
}
}
/* GPU acceleration for animated elements */
.animate-float,
.animate-pulse,
.animate-glow {
will-change: transform, opacity;
transform: translateZ(0);
backface-visibility: hidden;
}
/* ===================================
Global Resets
=================================== */
*, *::before, *::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ===================================
Radzen Layout Overrides
=================================== */
.rz-layout {
min-height: 100vh;
background: var(--bg-primary) !important;
}
.rz-sidebar {
width: 280px !important;
min-width: 280px !important;
border-right: none !important;
}
.rz-body {
padding: 0 !important;
}
.rz-header {
display: none !important;
}
@media (max-width: 1023px) {
.rz-header {
display: flex !important;
background: var(--bg-secondary) !important;
border-bottom: 1px solid var(--border-color) !important;
}
.rz-sidebar {
position: fixed !important;
z-index: 1000;
height: 100vh;
}
}
/* ===================================
Radzen Panel Menu (Navigation)
=================================== */
.rz-panel-menu {
background: transparent !important;
padding: 0.5rem !important;
}
.rz-panel-menu .rz-navigation-item-wrapper {
border-radius: 12px !important;
margin: 2px 0 !important;
padding: 0.875rem 1rem !important;
color: rgba(255, 255, 255, 0.75) !important;
transition: all 0.2s ease !important;
background: transparent !important;
}
.rz-panel-menu .rz-navigation-item-wrapper:hover {
background: rgba(255, 255, 255, 0.1) !important;
color: white !important;
}
.rz-panel-menu .rz-navigation-item-wrapper-active {
background: rgba(255, 255, 255, 0.15) !important;
color: white !important;
}
.rz-panel-menu .rz-navigation-item-icon {
color: rgba(255, 255, 255, 0.9) !important;
margin-right: 0.75rem !important;
}
.rz-panel-menu .rz-navigation-item-text {
font-weight: 500 !important;
color: inherit !important;
}
.rz-panel-menu .rz-panel-menu-content {
background: transparent !important;
padding-left: 0.75rem !important;
}
/* ===================================
Radzen Form Controls
=================================== */
.rz-textbox,
.rz-textarea,
.rz-dropdown,
.rz-inputtext {
background: var(--bg-tertiary) !important;
border: 1px solid var(--border-color) !important;
border-radius: 12px !important;
color: var(--text-primary) !important;
transition: all 0.2s ease !important;
}
.rz-textbox:focus-within,
.rz-textarea:focus-within,
.rz-password:focus-within {
border-color: var(--primary) !important;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2) !important;
}
.rz-textbox input::placeholder,
.rz-textarea textarea::placeholder {
color: #64748b !important;
}
/* Password field */
.rz-password {
background: var(--bg-tertiary) !important;
border: 1px solid var(--border-color) !important;
border-radius: 12px !important;
}
.rz-password input {
color: var(--text-primary) !important;
}
/* ===================================
Radzen Buttons
=================================== */
.rz-button {
border-radius: 12px !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
}
.rz-button:disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
}
/* ===================================
Radzen Switch
=================================== */
.rz-switch.rz-switch-checked .rz-switch-circle {
background: var(--primary) !important;
}
/* ===================================
Radzen DataGrid
=================================== */
.rz-datatable {
background: transparent !important;
border: none !important;
border-radius: 16px !important;
overflow: hidden;
}
.rz-datatable .rz-datatable-thead > tr > th {
background: linear-gradient(135deg, var(--primary), var(--accent)) !important;
color: white !important;
font-weight: 600 !important;
border: none !important;
padding: 1rem !important;
}
.rz-datatable .rz-datatable-data > tr {
background: transparent !important;
}
.rz-datatable .rz-datatable-data > tr:hover {
background: rgba(255, 255, 255, 0.05) !important;
}
.rz-datatable .rz-datatable-data > tr > td {
border-color: var(--border-color) !important;
padding: 0.875rem 1rem !important;
color: var(--text-primary) !important;
}
/* ===================================
Radzen Progress Components
=================================== */
.rz-progressbar {
background: var(--bg-tertiary) !important;
border-radius: 8px !important;
}
.rz-progressbar-value {
background: linear-gradient(90deg, var(--primary), var(--accent)) !important;
border-radius: 8px !important;
}
/* ===================================
Radzen Card
=================================== */
.rz-card {
background: var(--bg-secondary) !important;
border: 1px solid var(--border-color) !important;
border-radius: 16px !important;
}
/* ===================================
Radzen Badge
=================================== */
.rz-badge {
border-radius: 8px !important;
font-weight: 500 !important;
}
/* ===================================
Custom Scrollbar
=================================== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* Thin scrollbar for sidebars */
.scrollbar-thin::-webkit-scrollbar {
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
}
/* ===================================
Blazor Error UI
=================================== */
#blazor-error-ui {
display: none;
}
/* ===================================
Line Clamp Utility (for plugin descriptions)
=================================== */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===================================
Console Component Styling
=================================== */
.console-container {
background: #0d1117 !important;
border-color: #21262d !important;
}