Updated WebUI with Radzen and tailwind css
This commit is contained in:
@@ -1,22 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark rz-dark">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="app.css"/>
|
||||
|
||||
<!-- Tailwind CSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<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="icon" type="image/png" href="favicon.png"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="rz-background-color font-sans antialiased min-h-screen rz-dark">
|
||||
<Routes/>
|
||||
<script src="_content/Radzen.Blazor/Radzen.Blazor.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>
|
||||
|
||||
</html>
|
||||
@@ -1,21 +1,59 @@
|
||||
@using WebUI.Components.Notification
|
||||
@inherits LayoutComponentBase
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<NotificationDisplay />
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<RadzenLayout>
|
||||
<RadzenHeader>
|
||||
<MobileHeader />
|
||||
</RadzenHeader>
|
||||
|
||||
<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">
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- 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>
|
||||
|
||||
<main>
|
||||
<article class="content px-4">
|
||||
<!-- 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
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
</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 id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
@code {
|
||||
private bool _sidebarExpanded = true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,11 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid align-items-center">
|
||||
<a class="navbar-brand align-content-center" href="">Seth Discord Bot</a>
|
||||
</div>
|
||||
</div>
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,49 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
<PageTitle>Error - Seth Bot</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
<RadzenStack AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Gap="1.5rem" class="animate-fade-in" Style="min-height: 70vh;">
|
||||
<div class="sidebar-logo-icon" style="width: 80px; height: 80px; background: linear-gradient(135deg, #ef4444, #f43f5e);">
|
||||
<RadzenIcon Icon="error" Style="font-size: 2.5rem;" />
|
||||
</div>
|
||||
|
||||
<RadzenText TextStyle="TextStyle.H3" TextAlign="TextAlign.Center"><strong>Oops!</strong></RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" TextAlign="TextAlign.Center" class="rz-color-secondary">An error occurred while processing your request.</RadzenText>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
<RadzenCard class="card-modern" Style="padding: 1rem;">
|
||||
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary">Request ID</RadzenText>
|
||||
<code>@RequestId</code>
|
||||
</RadzenCard>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
<RadzenCard class="card-modern" Style="max-width: 500px;">
|
||||
<RadzenStack Gap="0.75rem">
|
||||
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0.5rem">
|
||||
<RadzenIcon Icon="info" Style="color: var(--rz-info);" />
|
||||
<RadzenText><strong>Development Mode</strong></RadzenText>
|
||||
</RadzenStack>
|
||||
<RadzenText TextStyle="TextStyle.Body2" class="rz-color-secondary">
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error.
|
||||
</RadzenText>
|
||||
<RadzenAlert AlertStyle="AlertStyle.Warning" Variant="Variant.Flat" Shade="Shade.Lighter" Size="AlertSize.Small">
|
||||
The Development environment shouldn't be enabled for deployed applications as it may expose sensitive information.
|
||||
</RadzenAlert>
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
|
||||
<RadzenButton Text="Back to Home"
|
||||
Icon="home"
|
||||
ButtonStyle="ButtonStyle.Primary"
|
||||
Click="@(() => Navigation.NavigateTo("/"))"
|
||||
class="btn-gradient-primary" />
|
||||
</RadzenStack>
|
||||
|
||||
@code{
|
||||
[CascadingParameter] private HttpContext? HttpContext { get; set; }
|
||||
[Inject] private NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
@using DiscordBotCore.Bot
|
||||
@using DiscordBotCore.Logging
|
||||
@using DiscordBotCore.PluginManagement.Loading
|
||||
@using WebUI.Models
|
||||
@using WebUI.Services
|
||||
@implements IDisposable
|
||||
|
||||
@inject IDiscordBotApplication DiscordBotApplication
|
||||
@inject IPluginLoader PluginLoader
|
||||
@@ -13,73 +12,132 @@
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<link rel="stylesheet" href="Components/Pages/Home.css" />
|
||||
<div class="container-fluid d-flex flex-column p-3" style="height: 95vh;">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-success"
|
||||
@onclick="StartApplication"
|
||||
disabled="@IsRunning">
|
||||
<i class="bi bi-play-fill me-1"></i> Start Application
|
||||
</button>
|
||||
<PageTitle>Home - Seth Bot</PageTitle>
|
||||
|
||||
<button class="btn btn-danger"
|
||||
@onclick="StopApplication"
|
||||
disabled="@(!IsRunning)">
|
||||
<i class="bi bi-stop-fill me-1"></i> Stop Application
|
||||
</button>
|
||||
|
||||
<button class="btn btn-warning text-dark"
|
||||
@onclick="LoadPlugins"
|
||||
disabled="@(!IsRunning)">
|
||||
<i class="bi bi-plug-fill me-1"></i> Load Plugins
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary"
|
||||
@onclick="ClearLogs">
|
||||
<i class="bi bi-trash3-fill me-1"></i> Clear Logs
|
||||
</button>
|
||||
<div class="space-y-6">
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-2xl md:text-3xl font-bold text-white mb-1">Dashboard</h2>
|
||||
<p class="text-slate-400 text-sm">Monitor and control your Discord bot</p>
|
||||
</div>
|
||||
|
||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Console Log</h5>
|
||||
<span class="badge bg-secondary">@Logger.LogMessages.Count()</span>
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium @(IsRunning ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400")">
|
||||
<span class="w-2 h-2 rounded-full @(IsRunning ? "bg-emerald-400" : "bg-red-400")"></span>
|
||||
@(IsRunning ? "Online" : "Offline")
|
||||
</div>
|
||||
<div id="consoleLog"
|
||||
class="bg-dark text-white p-3 flex-grow-1 overflow-auto"
|
||||
style="font-family:monospace;">
|
||||
</div>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="bg-dark-secondary border border-slate-700/50 rounded-2xl p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="p-2 rounded-lg bg-primary/20">
|
||||
<RadzenIcon Icon="tune" class="text-primary" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white">Control Panel</h3>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<RadzenButton Text="Start Bot"
|
||||
Icon="play_arrow"
|
||||
ButtonStyle="ButtonStyle.Success"
|
||||
Disabled="@IsRunning"
|
||||
Click="@StartApplication"
|
||||
class="!bg-gradient-to-r !from-emerald-500 !to-teal-500" />
|
||||
|
||||
<RadzenButton Text="Stop Bot"
|
||||
Icon="stop"
|
||||
ButtonStyle="ButtonStyle.Danger"
|
||||
Disabled="@(!IsRunning)"
|
||||
Click="@StopApplication"
|
||||
class="!bg-gradient-to-r !from-red-500 !to-rose-500" />
|
||||
|
||||
<RadzenButton Text="Load Plugins"
|
||||
Icon="extension"
|
||||
ButtonStyle="ButtonStyle.Warning"
|
||||
Disabled="@(!IsRunning)"
|
||||
Click="@LoadPlugins"
|
||||
class="!bg-gradient-to-r !from-amber-500 !to-orange-500" />
|
||||
|
||||
<RadzenButton Text="Clear Logs"
|
||||
Icon="delete_sweep"
|
||||
ButtonStyle="ButtonStyle.Light"
|
||||
Click="@ClearLogs"
|
||||
class="!bg-slate-700 hover:!bg-slate-600 !border-slate-600" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Console Log -->
|
||||
<div class="console-container bg-[#0d1117] border border-[#21262d] rounded-2xl overflow-hidden">
|
||||
<div class="bg-[#161b22] px-5 py-4 border-b border-[#21262d] flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-primary/20">
|
||||
<RadzenIcon Icon="terminal" class="text-primary-light" />
|
||||
</div>
|
||||
<h3 class="text-white font-semibold">Console Log</h3>
|
||||
</div>
|
||||
<span class="px-3 py-1 text-xs font-medium rounded-full bg-slate-800 text-slate-300">
|
||||
@_logCount entries
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="consoleLog" class="p-4 font-mono text-sm leading-relaxed max-h-[400px] overflow-y-auto">
|
||||
@if (_logCount == 0)
|
||||
{
|
||||
<div class="flex flex-col items-center justify-center h-[200px] text-slate-500">
|
||||
<RadzenIcon Icon="inbox" class="text-5xl opacity-50 mb-2" />
|
||||
<p>No logs yet. Start the bot to see activity.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var line in Logger.LogMessages)
|
||||
{
|
||||
<div style="@GetLogStyle(line)">@line.Message</div>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
window.scrollToBottom = function (elementId) {
|
||||
var el = document.getElementById(elementId);
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsRunning { get; set; }
|
||||
private int _logCount;
|
||||
private System.Timers.Timer? _debounceTimer;
|
||||
private bool _pendingUpdate;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
IsRunning = DiscordBotApplication.IsReady;
|
||||
_logCount = Logger.LogMessages.Count();
|
||||
Logger.OnLogReceived += LoggerOnLogReceived;
|
||||
|
||||
// Debounce timer for log updates - prevents too many re-renders
|
||||
_debounceTimer = new System.Timers.Timer(100);
|
||||
_debounceTimer.Elapsed += async (s, e) =>
|
||||
{
|
||||
if (_pendingUpdate)
|
||||
{
|
||||
_pendingUpdate = false;
|
||||
await InvokeAsync(() =>
|
||||
{
|
||||
_logCount = Logger.LogMessages.Count();
|
||||
StateHasChanged();
|
||||
JS.InvokeVoidAsync("scrollToBottom", "consoleLog");
|
||||
});
|
||||
}
|
||||
};
|
||||
_debounceTimer.AutoReset = true;
|
||||
_debounceTimer.Start();
|
||||
}
|
||||
|
||||
private void LoggerOnLogReceived(ILogMessage obj)
|
||||
{
|
||||
InvokeAsync(async () =>
|
||||
{
|
||||
await JS.InvokeVoidAsync("scrollToBottom", "consoleLog");
|
||||
StateHasChanged();
|
||||
});
|
||||
_pendingUpdate = true;
|
||||
}
|
||||
|
||||
private async Task StartApplication()
|
||||
@@ -88,7 +146,7 @@
|
||||
{
|
||||
await DiscordBotApplication.StartAsync();
|
||||
Logger.Log("Application started", this);
|
||||
NotificationService.Notify("Bot Started !", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Bot Started!");
|
||||
}
|
||||
|
||||
IsRunning = DiscordBotApplication.IsReady;
|
||||
@@ -100,7 +158,7 @@
|
||||
{
|
||||
await DiscordBotApplication.StopAsync();
|
||||
Logger.Log("Application stopped", this);
|
||||
NotificationService.Notify("Bot Stopped !", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Bot Stopped!");
|
||||
}
|
||||
|
||||
IsRunning = DiscordBotApplication.IsReady;
|
||||
@@ -111,24 +169,43 @@
|
||||
Logger.Log("Loading plugins", this);
|
||||
await PluginLoader.LoadPlugins();
|
||||
Logger.Log("Plugins loaded", this);
|
||||
NotificationService.Notify("Plugins Loaded !", NotificationType.Success);
|
||||
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Plugins Loaded!");
|
||||
}
|
||||
|
||||
private string GetLogStyle(ILogMessage logMessage)
|
||||
private static string GetLogTailwindClass(ILogMessage logMessage)
|
||||
{
|
||||
return logMessage.LogMessageType switch
|
||||
{
|
||||
LogType.Info => "color: white;",
|
||||
LogType.Warning => "color: yellow;",
|
||||
LogType.Error => "color: red;",
|
||||
LogType.Critical => "color: purple;",
|
||||
_ => ""
|
||||
LogType.Info => "text-sky-400",
|
||||
LogType.Warning => "text-amber-400",
|
||||
LogType.Error => "text-red-400",
|
||||
LogType.Critical => "text-purple-400 font-semibold",
|
||||
_ => "text-sky-400"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetLogIcon(ILogMessage logMessage)
|
||||
{
|
||||
return logMessage.LogMessageType switch
|
||||
{
|
||||
LogType.Info => "ℹ",
|
||||
LogType.Warning => "⚠",
|
||||
LogType.Error => "✕",
|
||||
LogType.Critical => "☠",
|
||||
_ => "•"
|
||||
};
|
||||
}
|
||||
|
||||
private void ClearLogs()
|
||||
{
|
||||
Logger.LogMessages.Clear();
|
||||
_logCount = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Logger.OnLogReceived -= LoggerOnLogReceived;
|
||||
_debounceTimer?.Stop();
|
||||
_debounceTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,74 +5,181 @@
|
||||
@using DiscordBotCore.Logging
|
||||
@using DiscordBotCore.PluginManagement
|
||||
@using DiscordBotCore.PluginManagement.Models
|
||||
@using WebUI.Models
|
||||
@using WebUI.Services
|
||||
@using WebUI.Components.Shared
|
||||
|
||||
@inject NavigationManager Navigation
|
||||
@inject NotificationService NotificationService
|
||||
|
||||
@inject IPluginManager PluginManager
|
||||
@inject IConfiguration Configuration
|
||||
@inject ILogger Logger
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<CenteredCard Title="Upload plugin">
|
||||
<EditForm Model="@pluginModel" OnValidSubmit="HandleValidSubmit" FormName="pluginForm">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
<PageTitle>Add Local Plugin - Seth Bot</PageTitle>
|
||||
|
||||
<div class="form-group">
|
||||
<ModernLabel Text="Plugin Name"></ModernLabel>
|
||||
<RoundedTextBox @bind-Value="pluginModel.Name" Placeholder="Example Plugin"></RoundedTextBox>
|
||||
<div class="max-w-xl mx-auto space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-4">
|
||||
<div class="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-xl shadow-emerald-500/40">
|
||||
<RadzenIcon Icon="upload" class="text-white text-3xl" />
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<ModernLabel Text="Plugin version"></ModernLabel>
|
||||
<RoundedTextBox @bind-Value="pluginModel.Version" Placeholder="1.0.0"></RoundedTextBox>
|
||||
<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">
|
||||
<ModernCheckbox @bind-Checked="pluginModel.IsEnabled" Label="Enable Plugin"></ModernCheckbox>
|
||||
<!-- 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 class="form-group">
|
||||
<ModernFileUploader OnFileUploaded="HandleUploadedFile" AllowedFileTypes="@allowedFileTypes"/>
|
||||
@if (_message is not null)
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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)
|
||||
{
|
||||
<p class="mt-2 text-green-600">@_message</p>
|
||||
<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>
|
||||
|
||||
<button class="btn btn-primary" type="submit">Upload</button>
|
||||
</EditForm>
|
||||
</CenteredCard>
|
||||
<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 PluginUploadModel _pluginModel = new();
|
||||
private IBrowserFile? _selectedFile;
|
||||
private string? _uploadMessage;
|
||||
private const long MaxFileSize = 10 * 1024 * 1024;
|
||||
|
||||
private List<string> allowedFileTypes { get; set; } = new List<string> { ".dll"};
|
||||
private bool IsValidVersion => System.Text.RegularExpressions.Regex.IsMatch(_pluginModel.Version, @"^\d+\.\d+\.\d+$");
|
||||
|
||||
private string? _message;
|
||||
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 bool IsValidVersion => System.Text.RegularExpressions.Regex.IsMatch(pluginModel.Version ?? "", @"^\d+\.\d+\.\d+$");
|
||||
|
||||
private async Task HandleUploadedFile(IBrowserFile file)
|
||||
private async Task TriggerFileInput()
|
||||
{
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream().ReadAsync(buffer);
|
||||
_message = $"Uploaded file: {file.Name} ({file.Size} bytes)";
|
||||
selectedFile = file;
|
||||
await JS.InvokeVoidAsync("eval", "document.getElementById('pluginFileInput').click()");
|
||||
}
|
||||
|
||||
private void HandleFileChange(InputFileChangeEventArgs e)
|
||||
{
|
||||
var file = e.File;
|
||||
_uploadMessage = null;
|
||||
|
||||
if (!file.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_uploadMessage = "Invalid file type. Only .dll files are allowed.";
|
||||
_selectedFile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.Size > MaxFileSize)
|
||||
{
|
||||
_uploadMessage = "File too large. Maximum size is 10 MB.";
|
||||
_selectedFile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedFile = file;
|
||||
|
||||
// Auto-fill plugin name from file if empty
|
||||
if (string.IsNullOrEmpty(_pluginModel.Name))
|
||||
{
|
||||
_pluginModel.Name = Path.GetFileNameWithoutExtension(file.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
if (!IsValidVersion || selectedFile is null || string.IsNullOrEmpty(selectedFile.Name))
|
||||
if (!IsValidVersion || _selectedFile is null || string.IsNullOrEmpty(_selectedFile.Name))
|
||||
{
|
||||
NotificationService.Notify("Invalid field values. Please check the form.", NotificationType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", "Invalid field values. Please check the form.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,32 +188,32 @@
|
||||
if (string.IsNullOrEmpty(pluginsFolder))
|
||||
{
|
||||
Logger.Log("Plugins folder is not set in the configuration.", this, LogType.Error);
|
||||
NotificationService.Notify("Plugins folder is not set in the configuration.", NotificationType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", "Plugins folder is not set in the configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
string filePath = Path.Combine(pluginsFolder, selectedFile.Name);
|
||||
string filePath = Path.Combine(pluginsFolder, _selectedFile.Name);
|
||||
|
||||
await using var stream = selectedFile.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
|
||||
await using var stream = _selectedFile.OpenReadStream(maxAllowedSize: MaxFileSize);
|
||||
await using var fileStream = File.Create(filePath);
|
||||
await stream.CopyToAsync(fileStream);
|
||||
|
||||
LocalPlugin plugin = new LocalPlugin(pluginModel.Name, pluginModel.Version, filePath, new(), true, pluginModel.IsEnabled);
|
||||
LocalPlugin plugin = new LocalPlugin(_pluginModel.Name, _pluginModel.Version, filePath, new(), true, _pluginModel.IsEnabled);
|
||||
await PluginManager.AppendPluginToDatabase(plugin);
|
||||
|
||||
NotificationService.Notify($"Plugin {pluginModel.Name} uploaded successfully!", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {_pluginModel.Name} uploaded successfully!");
|
||||
|
||||
Navigation.NavigateTo("/");
|
||||
Navigation.NavigateTo("/plugins/local");
|
||||
}
|
||||
|
||||
public class PluginUploadModel
|
||||
private class PluginUploadModel
|
||||
{
|
||||
[Required]
|
||||
[Required(ErrorMessage = "Plugin name is required.")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[Required(ErrorMessage = "Version is required.")]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,74 +9,172 @@
|
||||
@inject IPluginManager PluginManager
|
||||
@inject ILogger Logger
|
||||
@inject IPluginLoader PluginLoader
|
||||
|
||||
@inject NavigationManager Navigation
|
||||
@inject NotificationService NotificationService
|
||||
@inject DialogService DialogService
|
||||
|
||||
<h3>Installed Plugins</h3>
|
||||
<button class="btn btn-outline-success" @onclick="NavigateToAddPlugin">
|
||||
<span class="bi me-1"></span> Add Local
|
||||
</button>
|
||||
<table class="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
<th>Offline Added</th>
|
||||
<th>Actions</th>
|
||||
@* <th> *@
|
||||
@* <button class="btn btn-outline-success" @onclick="NavigateToAddPlugin"> *@
|
||||
@* <span class="bi me-1"></span> Add Local *@
|
||||
@* </button> *@
|
||||
@* </th> *@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var plugin in _installedPlugins)
|
||||
<PageTitle>Local Plugins - Seth Bot</PageTitle>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-amber-500 to-orange-500 flex items-center justify-center shadow-lg shadow-amber-500/30">
|
||||
<RadzenIcon Icon="folder" class="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Local Plugins</h2>
|
||||
<p class="text-slate-400 text-sm">Manage your installed plugins</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadzenButton Text="Add Local Plugin"
|
||||
Icon="add"
|
||||
ButtonStyle="ButtonStyle.Success"
|
||||
Click="@NavigateToAddPlugin"
|
||||
class="!bg-gradient-to-r !from-emerald-500 !to-teal-500 !rounded-xl" />
|
||||
</div>
|
||||
|
||||
@if (_installedPlugins.Any())
|
||||
{
|
||||
<tr>
|
||||
<td>@plugin.Name</td>
|
||||
<td>@plugin.Version</td>
|
||||
<td>@(plugin.IsOfflineAdded ? "Yes" : "No")</td>
|
||||
<td >
|
||||
<button class="btn btn-danger btn-sm m-1" @onclick="async () => await DeletePluginButtonClick(plugin)">Delete</button>
|
||||
<!-- 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 class="btn btn-info btn-sm m-1" @onclick="async () => await PluginDetailsButtonClick(plugin)">Details</button>
|
||||
<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>
|
||||
}
|
||||
</td>
|
||||
@* <td></td> *@
|
||||
</tr>
|
||||
<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 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>
|
||||
<h3 class="font-semibold text-white">@_selectedPluginDetails.Name</h3>
|
||||
<p class="text-white/70 text-sm">v@_selectedPluginDetails.Version</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @onclick="ClosePluginDetailsModal">Close</button>
|
||||
</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>
|
||||
|
||||
<!-- 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="space-y-1">
|
||||
<p class="text-slate-400 text-xs uppercase tracking-wider">Description</p>
|
||||
<p class="text-white">@_selectedPluginDetails.Description</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-5 pt-0">
|
||||
<button @onclick="ClosePluginDetailsModal"
|
||||
class="w-full py-2.5 px-4 rounded-xl bg-slate-700 hover:bg-slate-600 text-white font-medium transition-all duration-200">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
private readonly List<InstalledPlugin> _installedPlugins = new List<InstalledPlugin>();
|
||||
|
||||
private readonly List<InstalledPlugin> _installedPlugins = new();
|
||||
private bool _showPluginDetailsModal;
|
||||
private OnlinePlugin? _selectedPluginDetails;
|
||||
|
||||
@@ -91,14 +189,15 @@
|
||||
if (!response.IsSuccess)
|
||||
{
|
||||
Logger.Log(response.Message, this, LogType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", response.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
_installedPlugins.RemoveAll(p => p.Name == plugin.Name);
|
||||
Logger.Log($"Plugin {plugin.Name} deleted", this);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {plugin.Name} deleted successfully!");
|
||||
|
||||
await PluginLoader.LoadPlugins();
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -121,7 +220,6 @@
|
||||
|
||||
private void ClosePluginDetailsModal()
|
||||
{
|
||||
Logger.Log("Closing plugin details modal", this);
|
||||
_showPluginDetailsModal = false;
|
||||
_selectedPluginDetails = null;
|
||||
StateHasChanged();
|
||||
@@ -156,8 +254,8 @@
|
||||
|
||||
private class InstalledPlugin
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public bool IsOfflineAdded { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,87 +4,121 @@
|
||||
@using DiscordBotCore.Logging
|
||||
@using DiscordBotCore.PluginManagement
|
||||
|
||||
@using WebUI.Models
|
||||
@using WebUI.Services
|
||||
|
||||
@inject NotificationService NotificationService
|
||||
@inject DialogService DialogService
|
||||
|
||||
<PageTitle>Online Plugins - Seth Bot</PageTitle>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<!-- Plugins Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@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>
|
||||
<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>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Loading...</p>
|
||||
<!-- 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="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 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="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">
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @onclick="CloseInstallPercentageModal">Cancel</button>
|
||||
</div>
|
||||
|
||||
<button @onclick="CloseInstallPercentageModal"
|
||||
class="w-full py-2.5 px-4 rounded-xl bg-slate-700 hover:bg-slate-600 text-white font-medium transition-all duration-200">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Inject]
|
||||
public IPluginManager PluginManager { get; set; }
|
||||
|
||||
[Inject]
|
||||
public ILogger Logger { get; set; }
|
||||
[Inject] public IPluginManager PluginManager { get; set; } = default!;
|
||||
[Inject] public ILogger Logger { get; set; } = default!;
|
||||
|
||||
private bool _showInstallPercentage;
|
||||
private float _installPercentage = 0f;
|
||||
private float _installPercentage;
|
||||
|
||||
private readonly List<OnlinePluginModel> _onlinePlugins = new();
|
||||
|
||||
@@ -132,7 +166,7 @@ else
|
||||
if (pluginData is null)
|
||||
{
|
||||
Logger.Log("Plugin data is null.", this, LogType.Error);
|
||||
NotificationService.Notify("Plugin data is null.", NotificationType.Error);
|
||||
NotificationService.Notify(NotificationSeverity.Error, "Error", "Plugin data is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,8 +182,7 @@ else
|
||||
await PluginManager.InstallPlugin(pluginData, progress);
|
||||
|
||||
Logger.Log($"Plugin {pluginData.Name} installed successfully.", this);
|
||||
|
||||
NotificationService.Notify($"Plugin {pluginData.Name} installed successfully!", NotificationType.Success);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", $"Plugin {pluginData.Name} installed successfully!");
|
||||
|
||||
CloseInstallPercentageModal();
|
||||
StateHasChanged();
|
||||
@@ -165,9 +198,9 @@ else
|
||||
private class OnlinePluginModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -2,43 +2,112 @@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using DiscordBotCore.Configuration
|
||||
@using DiscordBotCore.Logging
|
||||
@using WebUI.Components.Shared
|
||||
@inject NavigationManager Navigation
|
||||
@inject NotificationService NotificationService
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Settings - Seth Bot</PageTitle>
|
||||
|
||||
<div class="max-w-xl mx-auto space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-4">
|
||||
<div class="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-xl shadow-primary/40">
|
||||
<RadzenIcon Icon="settings" class="text-white text-3xl" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">Settings</h2>
|
||||
<p class="text-slate-400 text-sm">Configure your Discord bot settings</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_settingsViewModel is not null)
|
||||
{
|
||||
<CenteredCard Title="Settings">
|
||||
<EditForm Model="_settingsViewModel" OnValidSubmit="HandleSubmitTask">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary class="text-danger" />
|
||||
|
||||
<div class="mb-3">
|
||||
<ModernLabel Text="Token"/>
|
||||
<RoundedTextBox @bind-Value="_settingsViewModel.Token" Placeholder="Your bot token"/>
|
||||
<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>
|
||||
|
||||
<div class="mb-3">
|
||||
<ModernLabel Text="Prefix"/>
|
||||
<RoundedTextBox @bind-Value="_settingsViewModel.Prefix" Placeholder="Your bot prefix"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<ModernLabel Text="Server IDs (command-separated)"/>
|
||||
<RoundedTextBox @bind-Value="_settingsViewModel.ServerIds" Placeholder="1234,5678" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
Save
|
||||
<!-- 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>
|
||||
</EditForm>
|
||||
</CenteredCard>
|
||||
}
|
||||
</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>
|
||||
|
||||
@code {
|
||||
[Inject] public ILogger Logger { get; set; }
|
||||
[Inject] public IConfiguration Configuration { get; set; }
|
||||
[Inject] public ILogger Logger { get; set; } = default!;
|
||||
[Inject] public IConfiguration Configuration { get; set; } = default!;
|
||||
|
||||
private SettingsViewModel? _settingsViewModel;
|
||||
|
||||
@@ -83,6 +152,7 @@
|
||||
|
||||
await Configuration.SaveToFile();
|
||||
Logger.Log("Settings saved successfully.", this);
|
||||
NotificationService.Notify(NotificationSeverity.Success, "Success", "Settings saved successfully!");
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
|
||||
@@ -94,7 +164,6 @@
|
||||
[Required(ErrorMessage = "Prefix is required.")]
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Server IDs are required.")]
|
||||
public string ServerIds { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using Radzen
|
||||
@using Radzen.Blazor
|
||||
@using WebUI
|
||||
@using WebUI.Components
|
||||
@using WebUI.Components.Layout
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace WebUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The notification type
|
||||
/// </summary>
|
||||
public enum NotificationType
|
||||
{
|
||||
Success,
|
||||
Error,
|
||||
Warning,
|
||||
Info
|
||||
}
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
using System.Reflection;
|
||||
using Radzen;
|
||||
using WebUI;
|
||||
using WebUI.Services;
|
||||
using WebUI.Components;
|
||||
|
||||
#region Load External (Unmanaged) Assemblies
|
||||
@@ -69,7 +69,9 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.AddDiscordBotComponents();
|
||||
builder.Services.AddSingleton<NotificationService>();
|
||||
|
||||
// Add Radzen services
|
||||
builder.Services.AddRadzenComponents();
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,13 @@
|
||||
</Content>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user