Improved UI experience for the Main page

This commit is contained in:
2025-04-06 18:36:20 +03:00
parent 8aaefac706
commit 87c889266b
8 changed files with 176 additions and 48 deletions

View File

@@ -1,3 +1,4 @@
using Discord.WebSocket;
using DiscordBotCore.PluginCore; using DiscordBotCore.PluginCore;
using DiscordBotCore.PluginCore.Interfaces; using DiscordBotCore.PluginCore.Interfaces;
@@ -9,4 +10,6 @@ public interface IPluginLoader
List<IDbEvent> Events { get; } List<IDbEvent> Events { get; }
List<IDbSlashCommand> SlashCommands { get; } List<IDbSlashCommand> SlashCommands { get; }
Task LoadPlugins(); Task LoadPlugins();
void SetClient(DiscordSocketClient client);
} }

View File

@@ -13,27 +13,19 @@ namespace DiscordBotCore.PluginManagement.Loading;
public sealed class PluginLoader : IPluginLoader public sealed class PluginLoader : IPluginLoader
{ {
private readonly DiscordSocketClient _DiscordClient;
private readonly IPluginManager _PluginManager; private readonly IPluginManager _PluginManager;
private readonly ILogger _Logger; private readonly ILogger _Logger;
private readonly IConfiguration _Configuration; private readonly IConfiguration _Configuration;
public delegate void CommandLoaded(IDbCommand eCommand);
public delegate void EventLoaded(IDbEvent eEvent);
public delegate void SlashCommandLoaded(IDbSlashCommand eSlashCommand);
public CommandLoaded? OnCommandLoaded;
public EventLoaded? OnEventLoaded;
public SlashCommandLoaded? OnSlashCommandLoaded;
public List<IDbCommand> Commands { get; private set; } = new List<IDbCommand>(); public List<IDbCommand> Commands { get; private set; } = new List<IDbCommand>();
public List<IDbEvent> Events { get; private set; } = new List<IDbEvent>(); public List<IDbEvent> Events { get; private set; } = new List<IDbEvent>();
public List<IDbSlashCommand> SlashCommands { get; private set; } = new List<IDbSlashCommand>(); public List<IDbSlashCommand> SlashCommands { get; private set; } = new List<IDbSlashCommand>();
public PluginLoader(IPluginManager pluginManager, ILogger logger, IConfiguration configuration, DiscordSocketClient discordSocketDiscordClient) private DiscordSocketClient? _discordClient;
public PluginLoader(IPluginManager pluginManager, ILogger logger, IConfiguration configuration)
{ {
_PluginManager = pluginManager; _PluginManager = pluginManager;
_DiscordClient = discordSocketDiscordClient;
_Logger = logger; _Logger = logger;
_Configuration = configuration; _Configuration = configuration;
} }
@@ -54,6 +46,24 @@ public sealed class PluginLoader : IPluginLoader
await loader.Load(); await loader.Load();
} }
public void SetClient(DiscordSocketClient client)
{
if (_discordClient is not null)
{
_Logger.Log("A client is already set. Please set the client only once.", this, LogType.Error);
return;
}
if (client.LoginState != LoginState.LoggedIn)
{
_Logger.Log("Client is not logged in. Retry after the client is logged in", this, LogType.Error);
return;
}
_Logger.Log("Client is set to the plugin loader", this);
_discordClient = client;
}
private void FileLoadedException(string fileName, Exception exception) private void FileLoadedException(string fileName, Exception exception)
{ {
_Logger.LogException(exception, this); _Logger.LogException(exception, this);
@@ -62,7 +72,7 @@ public sealed class PluginLoader : IPluginLoader
private void InitializeDbCommand(IDbCommand command) private void InitializeDbCommand(IDbCommand command)
{ {
Commands.Add(command); Commands.Add(command);
OnCommandLoaded?.Invoke(command); _Logger.Log("Command loaded: " + command.Command, this);
} }
private void InitializeEvent(IDbEvent eEvent) private void InitializeEvent(IDbEvent eEvent)
@@ -73,7 +83,7 @@ public sealed class PluginLoader : IPluginLoader
} }
Events.Add(eEvent); Events.Add(eEvent);
OnEventLoaded?.Invoke(eEvent); _Logger.Log("Event loaded: " + eEvent, this);
} }
private async void InitializeSlashCommand(IDbSlashCommand slashCommand) private async void InitializeSlashCommand(IDbSlashCommand slashCommand)
@@ -83,9 +93,9 @@ public sealed class PluginLoader : IPluginLoader
() => () =>
{ {
if (slashCommand.HasInteraction) if (slashCommand.HasInteraction)
_DiscordClient.InteractionCreated += interaction => slashCommand.ExecuteInteraction(_Logger, interaction); _discordClient.InteractionCreated += interaction => slashCommand.ExecuteInteraction(_Logger, interaction);
SlashCommands.Add(slashCommand); SlashCommands.Add(slashCommand);
OnSlashCommandLoaded?.Invoke(slashCommand); _Logger.Log("Slash command loaded: " + slashCommand.Name, this);
}, },
HandleError HandleError
); );
@@ -116,7 +126,7 @@ public sealed class PluginLoader : IPluginLoader
} }
IDbEventExecutingArgument args = new DbEventExecutingArgument( IDbEventExecutingArgument args = new DbEventExecutingArgument(
_Logger, _Logger,
_DiscordClient, _discordClient,
_Configuration.Get<string>("prefix"), _Configuration.Get<string>("prefix"),
new DirectoryInfo(Path.Combine(_Configuration.Get<string>("ResourcesPath"), dbEvent.Name))); new DirectoryInfo(Path.Combine(_Configuration.Get<string>("ResourcesPath"), dbEvent.Name)));
dbEvent.Start(args); dbEvent.Start(args);
@@ -139,7 +149,7 @@ public sealed class PluginLoader : IPluginLoader
return Result.Failure(new Exception("dbSlashCommand is null")); return Result.Failure(new Exception("dbSlashCommand is null"));
} }
if (_DiscordClient.Guilds.Count == 0) if (_discordClient.Guilds.Count == 0)
{ {
return Result.Failure(new Exception("No guilds found")); return Result.Failure(new Exception("No guilds found"));
} }
@@ -166,7 +176,7 @@ public sealed class PluginLoader : IPluginLoader
} }
} }
await _DiscordClient.CreateGlobalApplicationCommandAsync(builder.Build()); await _discordClient.CreateGlobalApplicationCommandAsync(builder.Build());
return Result.Success(); return Result.Success();
} }
@@ -178,7 +188,7 @@ public sealed class PluginLoader : IPluginLoader
private async Task<bool> EnableSlashCommandPerGuild(ulong guildId, SlashCommandBuilder builder) private async Task<bool> EnableSlashCommandPerGuild(ulong guildId, SlashCommandBuilder builder)
{ {
SocketGuild? guild = _DiscordClient.GetGuild(guildId); SocketGuild? guild = _discordClient.GetGuild(guildId);
if (guild is null) if (guild is null)
{ {
_Logger.Log("Failed to get guild with ID " + guildId, typeof(PluginLoader), LogType.Error); _Logger.Log("Failed to get guild with ID " + guildId, typeof(PluginLoader), LogType.Error);

View File

@@ -20,7 +20,7 @@ public class DiscordBotApplication : IDiscordBotApplication
private readonly IConfiguration _Configuration; private readonly IConfiguration _Configuration;
private readonly IPluginLoader _PluginLoader; private readonly IPluginLoader _PluginLoader;
private bool IsReady { get; set; } public bool IsReady { get; private set; }
public DiscordSocketClient Client { get; private set; } public DiscordSocketClient Client { get; private set; }
@@ -34,6 +34,28 @@ public class DiscordBotApplication : IDiscordBotApplication
this._PluginLoader = pluginLoader; this._PluginLoader = pluginLoader;
} }
public async Task StopAsync()
{
if (!IsReady)
{
_Logger.Log("Can not stop the bot. It is not yet initialized.", this, LogType.Error);
return;
}
await Client.LogoutAsync();
await Client.StopAsync();
Client.Log -= Log;
Client.LoggedIn -= LoggedIn;
Client.Ready -= Ready;
Client.Disconnected -= Client_Disconnected;
await Client.DisposeAsync();
IsReady = false;
}
/// <summary> /// <summary>
/// The start method for the bot. This method is used to load the bot /// The start method for the bot. This method is used to load the bot
/// </summary> /// </summary>
@@ -91,6 +113,7 @@ public class DiscordBotApplication : IDiscordBotApplication
private Task LoggedIn() private Task LoggedIn()
{ {
_Logger.Log("Successfully Logged In", this); _Logger.Log("Successfully Logged In", this);
_PluginLoader.SetClient(Client);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -5,10 +5,16 @@ namespace DiscordBotCore.Bot;
public interface IDiscordBotApplication public interface IDiscordBotApplication
{ {
public bool IsReady { get; }
public DiscordSocketClient Client { get; } public DiscordSocketClient Client { get; }
/// <summary> /// <summary>
/// The start method for the bot. This method is used to load the bot /// The start method for the bot. This method is used to load the bot
/// </summary> /// </summary>
Task StartAsync(); Task StartAsync();
/// <summary>
/// Stops the bot and cleans up resources.
/// </summary>
Task StopAsync();
} }

View File

@@ -1,6 +1,6 @@
using System.Diagnostics; using DiscordBotCore.Bot;
using DiscordBotCore.PluginManagement.Loading;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using WebUI.Models;
using ILogger = DiscordBotCore.Logging.ILogger; using ILogger = DiscordBotCore.Logging.ILogger;
namespace WebUI.Controllers; namespace WebUI.Controllers;
@@ -8,25 +8,64 @@ namespace WebUI.Controllers;
public class HomeController : Controller public class HomeController : Controller
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
public HomeController(ILogger logger) private readonly IDiscordBotApplication _discordBotApplication;
private readonly IPluginLoader _pluginLoader;
public HomeController(ILogger logger, IDiscordBotApplication discordBotApplication, IPluginLoader pluginLoader)
{ {
_logger = logger; _logger = logger;
_discordBotApplication = discordBotApplication;
_pluginLoader = pluginLoader;
} }
[HttpGet]
public IActionResult Index() public IActionResult Index()
{ {
_logger.Log("Index page loaded", this); _logger.Log("Index page loaded", this);
ViewBag.IsRunning = _discordBotApplication.IsReady;
return View(); return View();
} }
public IActionResult Privacy() [HttpPost]
public async Task<IActionResult> StartApplication()
{ {
return View(); if (_discordBotApplication.IsReady)
} {
_logger.Log("Application already started", this);
return RedirectToAction("Index");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] await _discordBotApplication.StartAsync();
public IActionResult Error() return RedirectToAction("Index");
}
[HttpPost]
public async Task<IActionResult> StopApplication()
{ {
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); if (!_discordBotApplication.IsReady)
{
_logger.Log("Application already stopped", this);
return RedirectToAction("Index");
}
await _discordBotApplication.StopAsync();
return RedirectToAction("Index");
}
[HttpGet]
public JsonResult GetLogs()
{
var logText = _logger.GetLogsHistory();
var logs = logText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
return Json(logs);
}
[HttpPost]
public async Task<IActionResult> LoadPlugins()
{
_logger.Log("Loading plugins", this);
await _pluginLoader.LoadPlugins();
_logger.Log("Plugins loaded", this);
return RedirectToAction("Index");
} }
} }

View File

@@ -142,6 +142,15 @@ builder.Services.AddSingleton<IPluginManager>(sp =>
IPluginManager pluginManager = new PluginManager(pluginRepository, logger, configuration); IPluginManager pluginManager = new PluginManager(pluginRepository, logger, configuration);
return pluginManager; return pluginManager;
}); });
builder.Services.AddSingleton<IPluginLoader>(sp =>
{
IPluginManager pluginManager = sp.GetRequiredService<IPluginManager>();
ILogger logger = sp.GetRequiredService<ILogger>();
IConfiguration configuration = sp.GetRequiredService<IConfiguration>();
return new PluginLoader(pluginManager, logger, configuration);
});
builder.Services.AddSingleton<IDiscordBotApplication>(sp => builder.Services.AddSingleton<IDiscordBotApplication>(sp =>
{ {
ILogger logger = sp.GetRequiredService<ILogger>(); ILogger logger = sp.GetRequiredService<ILogger>();
@@ -150,14 +159,7 @@ builder.Services.AddSingleton<IDiscordBotApplication>(sp =>
return new DiscordBotApplication(logger, configuration, pluginLoader); return new DiscordBotApplication(logger, configuration, pluginLoader);
}); });
builder.Services.AddSingleton<IPluginLoader>(sp =>
{
IPluginManager pluginManager = sp.GetRequiredService<IPluginManager>();
ILogger logger = sp.GetRequiredService<ILogger>();
IConfiguration configuration = sp.GetRequiredService<IConfiguration>();
IDiscordBotApplication discordBotApplication = sp.GetRequiredService<IDiscordBotApplication>();
return new PluginLoader(pluginManager, logger, configuration, discordBotApplication.Client);
});
var app = builder.Build(); var app = builder.Build();

View File

@@ -1,8 +1,59 @@
@{ @{
ViewData["Title"] = "Home Page"; var isRunning = ViewBag.IsRunning ?? false;
ViewBag.Title = "Console Log Viewer";
} }
<div class="text-center"> <div class="container mt-5">
<h1 class="display-4">Welcome</h1> <div class="row">
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> <div class="col-md-8">
</div> <h4>Console Log</h4>
<div id="consoleLog" class="border p-3 bg-dark text-white" style="height: 400px; overflow-y: scroll; font-family: monospace;"></div>
</div>
<div class="col-md-4">
<h4>Controls</h4>
<form method="post" asp-action="StartApplication">
<button type="submit" class="btn btn-success mb-2">Start Application</button>
</form>
@if (isRunning)
{
<form method="post" asp-action="StopApplication">
<button type="submit" class="btn btn-danger mb-2">Stop Application</button>
</form>
<form method="post" asp-action="LoadPlugins">
<button type="submit" class="btn btn-info mb-2">Load Plugins</button>
</form>
}
</div>
</div>
</div>
@section Scripts {
<script>
let lastLogCount = 0;
function fetchLogs() {
fetch("/Home/GetLogs")
.then(res => res.json())
.then(logs => {
const logDiv = document.getElementById("consoleLog");
if (logs.length !== lastLogCount) {
logDiv.innerHTML = logs.map(line => formatLog(line)).join('');
logDiv.scrollTop = logDiv.scrollHeight;
lastLogCount = logs.length;
}
});
}
function formatLog(line) {
if (line.includes("[Error]")) return `<div style="color: #ff4d4d;">${line}</div>`;
if (line.includes("[Warning]")) return `<div style="color: #ffa500;">${line}</div>`;
if (line.includes("[Debug]")) return `<div style="color: #88f;">${line}</div>`;
return `<div>${line}</div>`;
}
setInterval(fetchLogs, 2000);
fetchLogs();
</script>
}

View File

@@ -1,6 +0,0 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>