diff --git a/.gitignore b/.gitignore index bee968e..23dd2bd 100644 --- a/.gitignore +++ b/.gitignore @@ -455,4 +455,7 @@ $RECYCLE.BIN/ /DiscordBotWebUI/Data /DiscordBot/Data -/WebUI/Data \ No newline at end of file +/WebUI/Data +/WebUI_Old/Data +/WebUI/bin +/WebUI_Old/bin \ No newline at end of file diff --git a/DiscordBotCore.PluginManagement/PluginManager.cs b/DiscordBotCore.PluginManagement/PluginManager.cs index de61de7..c818743 100644 --- a/DiscordBotCore.PluginManagement/PluginManager.cs +++ b/DiscordBotCore.PluginManagement/PluginManager.cs @@ -108,6 +108,7 @@ public sealed class PluginManager : IPluginManager if (!File.Exists(pluginDatabaseFile)) { _Logger.Log("Plugin database file not found", this, LogType.Warning); + await CreateEmptyPluginDatabase(); return []; } @@ -253,4 +254,25 @@ public sealed class PluginManager : IPluginManager await AppendPluginToDatabase(plugin); } + + private async Task CreateEmptyPluginDatabase() + { + string ? pluginDatabaseFile = _Configuration.Get("PluginDatabase"); + if (pluginDatabaseFile is null) + { + _Logger.Log("Plugin database file path is not present in the config file", this, LogType.Warning); + return false; + } + + if (File.Exists(pluginDatabaseFile)) + { + _Logger.Log("Plugin database file already exists", this, LogType.Warning); + return false; + } + + List installedPlugins = new List(); + await JsonManager.SaveToJsonFile(pluginDatabaseFile, installedPlugins); + _Logger.Log("Plugin database file created", this, LogType.Info); + return true; + } } diff --git a/SethDiscordBot.sln b/SethDiscordBot.sln index a899514..71635b0 100644 --- a/SethDiscordBot.sln +++ b/SethDiscordBot.sln @@ -31,12 +31,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotCore.Configuratio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotCore.PluginManagement.Loading", "DiscordBotCore.PluginManagement.Loading\DiscordBotCore.PluginManagement.Loading.csproj", "{E8ED73E1-F7D9-44E7-9542-21BC86338724}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUI", "WebUI\WebUI.csproj", "{F2E2C2D7-C030-4350-907F-86D5B2AAEA0B}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotCore.Database.Sqlite", "DiscordBotCore.Database.Sqlite\DiscordBotCore.Database.Sqlite.csproj", "{6D43E9A7-A295-41AC-8B2A-9A877FABB5DE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotCore.WebApplication", "DiscordBotCore.WebApplication\DiscordBotCore.WebApplication.csproj", "{A65A1D7A-99E9-463F-A205-F96CA54D5C51}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUI_OLD", "WebUI_Old\WebUI_OLD.csproj", "{1D58D773-9BB0-422D-8819-83AF0DF7CCA8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUI", "WebUI\WebUI.csproj", "{DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,10 +89,6 @@ Global {E8ED73E1-F7D9-44E7-9542-21BC86338724}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8ED73E1-F7D9-44E7-9542-21BC86338724}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8ED73E1-F7D9-44E7-9542-21BC86338724}.Release|Any CPU.Build.0 = Release|Any CPU - {F2E2C2D7-C030-4350-907F-86D5B2AAEA0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F2E2C2D7-C030-4350-907F-86D5B2AAEA0B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F2E2C2D7-C030-4350-907F-86D5B2AAEA0B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F2E2C2D7-C030-4350-907F-86D5B2AAEA0B}.Release|Any CPU.Build.0 = Release|Any CPU {6D43E9A7-A295-41AC-8B2A-9A877FABB5DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D43E9A7-A295-41AC-8B2A-9A877FABB5DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D43E9A7-A295-41AC-8B2A-9A877FABB5DE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -99,6 +97,14 @@ Global {A65A1D7A-99E9-463F-A205-F96CA54D5C51}.Debug|Any CPU.Build.0 = Debug|Any CPU {A65A1D7A-99E9-463F-A205-F96CA54D5C51}.Release|Any CPU.ActiveCfg = Release|Any CPU {A65A1D7A-99E9-463F-A205-F96CA54D5C51}.Release|Any CPU.Build.0 = Release|Any CPU + {1D58D773-9BB0-422D-8819-83AF0DF7CCA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D58D773-9BB0-422D-8819-83AF0DF7CCA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D58D773-9BB0-422D-8819-83AF0DF7CCA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D58D773-9BB0-422D-8819-83AF0DF7CCA8}.Release|Any CPU.Build.0 = Release|Any CPU + {DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WebUI/Components/App.razor b/WebUI/Components/App.razor new file mode 100644 index 0000000..f8e7e61 --- /dev/null +++ b/WebUI/Components/App.razor @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WebUI/Components/Layout/MainLayout.razor b/WebUI/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..7ff1bf0 --- /dev/null +++ b/WebUI/Components/Layout/MainLayout.razor @@ -0,0 +1,19 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
\ No newline at end of file diff --git a/WebUI/Components/Layout/MainLayout.razor.css b/WebUI/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..038baf1 --- /dev/null +++ b/WebUI/Components/Layout/MainLayout.razor.css @@ -0,0 +1,96 @@ +.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; + } diff --git a/WebUI/Components/Layout/NavMenu.razor b/WebUI/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..00fc318 --- /dev/null +++ b/WebUI/Components/Layout/NavMenu.razor @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file diff --git a/WebUI/Components/Layout/NavMenu.razor.css b/WebUI/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..4e15395 --- /dev/null +++ b/WebUI/Components/Layout/NavMenu.razor.css @@ -0,0 +1,105 @@ +.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; + } +} diff --git a/WebUI/Components/Pages/Error.razor b/WebUI/Components/Pages/Error.razor new file mode 100644 index 0000000..9d7c6be --- /dev/null +++ b/WebUI/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + +} \ No newline at end of file diff --git a/WebUI/Components/Pages/Home.razor b/WebUI/Components/Pages/Home.razor new file mode 100644 index 0000000..dfcdf75 --- /dev/null +++ b/WebUI/Components/Pages/Home.razor @@ -0,0 +1,7 @@ +@page "/" + +Home + +

Hello, world!

+ +Welcome to your new app. \ No newline at end of file diff --git a/WebUI/Components/Pages/Plugins/Local.razor b/WebUI/Components/Pages/Plugins/Local.razor new file mode 100644 index 0000000..f697c9e --- /dev/null +++ b/WebUI/Components/Pages/Plugins/Local.razor @@ -0,0 +1,142 @@ +@page "/plugins/local" +@using DiscordBotCore.Logging +@using DiscordBotCore.PluginManagement +@using DiscordBotCore.PluginManagement.Models + +

Installed Plugins

+ + + + + + + + + + + @foreach (var plugin in _installedPlugins) + { + + + + + + + } + +
NameVersionOffline AddedActions
@plugin.Name@plugin.Version@(plugin.IsOfflineAdded ? "Yes" : "No") + + +
+ +@if (_showPluginDetailsModal && _selectedPluginDetails != null) +{ + +} + + +@code { + [Inject] + public IPluginManager PluginManager { get; set; } + + [Inject] + public ILogger Logger { get; set; } + + private readonly List _installedPlugins = new List(); + + private bool _showPluginDetailsModal; + private OnlinePlugin? _selectedPluginDetails; + + private async Task DeletePluginButtonClick(string pluginName) + { + Logger.Log($"Deleting plugin {pluginName}", this); + + bool result = await PluginManager.MarkPluginToUninstall(pluginName); + + if (!result) + { + Logger.Log($"Failed to delete plugin {pluginName}", this, LogType.Error); + return; + } + + _installedPlugins.RemoveAll(p => p.Name == pluginName); + Logger.Log($"Plugin {pluginName} deleted", this); + StateHasChanged(); + } + + private async Task PluginDetailsButtonClick(string pluginName) + { + Logger.Log($"Getting plugin details for {pluginName}", this); + var pluginDetails = await PluginManager.GetPluginDataByName(pluginName); + if (pluginDetails == null) + { + Logger.Log($"Failed to get details for plugin {pluginName}", this, LogType.Error); + return; + } + + _selectedPluginDetails = pluginDetails; + _showPluginDetailsModal = true; + + Logger.Log($"Plugin details for {pluginName} retrieved", this); + StateHasChanged(); + } + + private void ClosePluginDetailsModal() + { + Logger.Log("Closing plugin details modal", this); + _showPluginDetailsModal = false; + _selectedPluginDetails = null; + StateHasChanged(); + } + + protected override async Task OnInitializedAsync() + { + Logger.Log("Local plugins page initialized", this); + var plugins = await PluginManager.GetInstalledPlugins(); + if (!plugins.Any()) + { + Logger.Log("No plugins found", this, LogType.Warning); + return; + } + + Logger.Log($"Found {plugins.Count} plugins", this); + _installedPlugins.Clear(); + + foreach (var plugin in plugins) + { + var installedPlugin = new InstalledPlugin + { + Name = plugin.PluginName, + Version = plugin.PluginVersion, + IsOfflineAdded = plugin.IsOfflineAdded + }; + _installedPlugins.Add(installedPlugin); + } + + StateHasChanged(); + } + + private class InstalledPlugin + { + public string Name { get; set; } + public string Version { get; set; } + public bool IsOfflineAdded { get; set; } + } +} \ No newline at end of file diff --git a/WebUI/Components/Pages/Plugins/Online.razor b/WebUI/Components/Pages/Plugins/Online.razor new file mode 100644 index 0000000..a4a285c --- /dev/null +++ b/WebUI/Components/Pages/Plugins/Online.razor @@ -0,0 +1,148 @@ +@page "/plugins/online" +@using DiscordBotCore.Logging +@using DiscordBotCore.PluginManagement + +

Available Plugins

+ +@if (_onlinePlugins.Any()) +{ + + + + + + + + + + + + @foreach (var plugin in _onlinePlugins) + { + + + + + + + + } + +
NameDescriptionAuthorVersionDownload
@plugin.Name@plugin.Description@plugin.Author@plugin.Version + +
+} +else +{ +

Loading...

+} + + +@if (_showInstallPercentage) +{ + +} + +@code { + [Inject] + public IPluginManager PluginManager { get; set; } + + [Inject] + public ILogger Logger { get; set; } + + private bool _showInstallPercentage; + private float _installPercentage = 0f; + + private readonly List _onlinePlugins = new(); + + protected override async Task OnInitializedAsync() + { + Logger.Log("Getting online plugins...", this); + var plugins = await PluginManager.GetPluginsList(); + + if (!plugins.Any()) + { + Logger.Log("No online plugins found.", this); + return; + } + + _onlinePlugins.Clear(); + + foreach (var plugin in plugins) + { + var onlinePlugin = new OnlinePluginModel + { + Id = plugin.Id, + Name = plugin.Name, + Description = plugin.Description, + Author = plugin.Author, + Version = plugin.Version, + }; + _onlinePlugins.Add(onlinePlugin); + } + } + + private async Task InstallPlugin(int pluginId) + { + var pluginData = await PluginManager.GetPluginDataById(pluginId); + if (pluginData == null) + { + Logger.Log($"Plugin data not found for ID: {pluginId}", this); + return; + } + + Logger.Log($"Installing plugin {pluginData.Name}...", this); + _showInstallPercentage = true; + + IProgress progress = new Progress(percent => + { + _installPercentage = percent; + StateHasChanged(); + }); + + await PluginManager.InstallPlugin(pluginData, progress); + + Logger.Log($"Plugin {pluginData.Name} installed successfully.", this); + CloseInstallPercentageModal(); + } + + private void CloseInstallPercentageModal() + { + Logger.Log("Closing install percentage modal", this); + _showInstallPercentage = false; + _installPercentage = 0f; + } + + 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; } + } +} diff --git a/WebUI/Components/Routes.razor b/WebUI/Components/Routes.razor new file mode 100644 index 0000000..0af9301 --- /dev/null +++ b/WebUI/Components/Routes.razor @@ -0,0 +1,11 @@ + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
\ No newline at end of file diff --git a/WebUI/Components/_Imports.razor b/WebUI/Components/_Imports.razor new file mode 100644 index 0000000..64656b8 --- /dev/null +++ b/WebUI/Components/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using WebUI +@using WebUI.Components \ No newline at end of file diff --git a/WebUI/Program.cs b/WebUI/Program.cs index adcc039..8fe10b4 100644 --- a/WebUI/Program.cs +++ b/WebUI/Program.cs @@ -1,120 +1,28 @@ -using System.Reflection; -using DiscordBotCore.Bot; -using DiscordBotCore.Configuration; -using DiscordBotCore.Logging; -using DiscordBotCore.PluginManagement; -using DiscordBotCore.PluginManagement.Helpers; -using DiscordBotCore.PluginManagement.Loading; using DiscordBotCore.WebApplication; -using IConfiguration = DiscordBotCore.Configuration.IConfiguration; -using ILogger = DiscordBotCore.Logging.ILogger; - -#region Load External (Unmanaged) Assemblies - -// This code is used to load external (unmanaged) assemblies from the same folder as the executing assembly. -// It handles the AssemblyResolve event to search for the requested assembly in a specific folder structure. -// The folder structure is expected to be: /Libraries//. -// The extensions to search for are specified in the 'extensions' parameter. - -var currentDomain = AppDomain.CurrentDomain; -currentDomain.AssemblyResolve += (sender, args) => LoadFromSameFolder(sender,args, [".dll", ".so", ".dylib"]); - -static Assembly? LoadFromSameFolder(object? sender, ResolveEventArgs args, string[] extensions) -{ - string? requestingAssemblyName = args.RequestingAssembly?.GetName().Name; - string executingAssemblyLocation = Assembly.GetExecutingAssembly().Location; - string? executingAssemblyDirectory = Path.GetDirectoryName(executingAssemblyLocation); - - if (string.IsNullOrEmpty(executingAssemblyDirectory)) - { - Console.WriteLine($"Error: Could not determine the directory of the executing assembly."); - return null; - } - - string librariesFolder = Path.Combine(executingAssemblyDirectory, "Libraries", requestingAssemblyName ?? ""); - string requestedAssemblyNameWithoutExtension = new AssemblyName(args.Name).Name; - - Console.WriteLine($"Requesting Assembly: {requestingAssemblyName}"); - Console.WriteLine($"Requested Assembly Name (without extension): {requestedAssemblyNameWithoutExtension}"); - Console.WriteLine($"Searching in folder: {librariesFolder}"); - Console.WriteLine($"Searching for extensions: {string.Join(", ", extensions)}"); - - foreach (string extension in extensions) - { - string assemblyFileName = requestedAssemblyNameWithoutExtension + extension; - string assemblyPath = Path.Combine(librariesFolder, assemblyFileName); - - Console.WriteLine($"Attempting to load from: {assemblyPath}"); - - if (File.Exists(assemblyPath)) - { - try - { - var fileAssembly = Assembly.LoadFrom(assemblyPath); - Console.WriteLine($"Successfully loaded Assembly: {fileAssembly.FullName}"); - return fileAssembly; - } - catch (Exception ex) - { - Console.WriteLine($"Error loading assembly from '{assemblyPath}': {ex.Message}"); - // Optionally log the full exception for debugging - } - } - else - { - Console.WriteLine($"File not found: {assemblyPath}"); - } - } - - Console.WriteLine($"Failed to load assembly '{args.Name}' from the specified locations."); - return null; -} -#endregion +using WebUI.Components; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddControllersWithViews(); +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); builder.AddDiscordBotComponents(); - var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Home/Error"); + app.UseExceptionHandler("/Error", createScopeForErrors: true); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); + app.UseStaticFiles(); +app.UseAntiforgery(); -app.UseRouting(); - -app.UseAuthorization(); - -app.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - -// Force eager creation of all required services -using (var scope = app.Services.CreateScope()) -{ - var provider = scope.ServiceProvider; - - // Manually resolve all your singletons here - provider.GetRequiredService(); - IConfiguration config = provider.GetRequiredService(); - provider.GetRequiredService(); - provider.GetRequiredService(); - provider.GetRequiredService(); - provider.GetRequiredService(); - provider.GetRequiredService(); - - // Optional: Log that all services were initialized - provider.GetRequiredService().Log("All core services have been initialized at startup."); -} - +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); app.Run(); \ No newline at end of file diff --git a/WebUI/Properties/launchSettings.json b/WebUI/Properties/launchSettings.json index b2c3329..2bd69a1 100644 --- a/WebUI/Properties/launchSettings.json +++ b/WebUI/Properties/launchSettings.json @@ -1,38 +1,38 @@ -{ +{ "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:52426", - "sslPort": 44369 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5111", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:41815", + "sslPort": 44333 } }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7222;http://localhost:5111", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5028", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7127;http://localhost:5028", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } -} diff --git a/WebUI/WebUI.csproj b/WebUI/WebUI.csproj index 36c8171..c263ed2 100644 --- a/WebUI/WebUI.csproj +++ b/WebUI/WebUI.csproj @@ -4,17 +4,9 @@ net8.0 enable enable - Linux - - .dockerignore - - - - - diff --git a/WebUI/appsettings.json b/WebUI/appsettings.json index fe95dc1..10f68b8 100644 --- a/WebUI/appsettings.json +++ b/WebUI/appsettings.json @@ -5,10 +5,5 @@ "Microsoft.AspNetCore": "Warning" } }, - "Logger": { - "LogFormat": "{ThrowTime} {SenderName} {Message}", - "LogFolder": "./Data/Logs" - }, - "ConfigFile": "./Data/Resources/config.json", "AllowedHosts": "*" } diff --git a/WebUI/wwwroot/app.css b/WebUI/wwwroot/app.css new file mode 100644 index 0000000..2bd9b78 --- /dev/null +++ b/WebUI/wwwroot/app.css @@ -0,0 +1,51 @@ +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; +} diff --git a/WebUI/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/WebUI/wwwroot/bootstrap/bootstrap.min.css similarity index 100% rename from WebUI/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to WebUI/wwwroot/bootstrap/bootstrap.min.css diff --git a/WebUI/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/WebUI/wwwroot/bootstrap/bootstrap.min.css.map similarity index 100% rename from WebUI/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to WebUI/wwwroot/bootstrap/bootstrap.min.css.map diff --git a/WebUI/wwwroot/favicon.png b/WebUI/wwwroot/favicon.png new file mode 100644 index 0000000..8422b59 Binary files /dev/null and b/WebUI/wwwroot/favicon.png differ diff --git a/WebUI/Controllers/HomeController.cs b/WebUI_Old/Controllers/HomeController.cs similarity index 98% rename from WebUI/Controllers/HomeController.cs rename to WebUI_Old/Controllers/HomeController.cs index 962a742..673c50c 100644 --- a/WebUI/Controllers/HomeController.cs +++ b/WebUI_Old/Controllers/HomeController.cs @@ -3,7 +3,7 @@ using DiscordBotCore.PluginManagement.Loading; using Microsoft.AspNetCore.Mvc; using ILogger = DiscordBotCore.Logging.ILogger; -namespace WebUI.Controllers; +namespace WebUI_OLD.Controllers; public class HomeController : Controller { diff --git a/WebUI/Controllers/PluginsController.cs b/WebUI_Old/Controllers/PluginsController.cs similarity index 98% rename from WebUI/Controllers/PluginsController.cs rename to WebUI_Old/Controllers/PluginsController.cs index acb5ffa..c46a00e 100644 --- a/WebUI/Controllers/PluginsController.cs +++ b/WebUI_Old/Controllers/PluginsController.cs @@ -1,9 +1,9 @@ using DiscordBotCore.PluginManagement; using Microsoft.AspNetCore.Mvc; -using WebUI.Models; +using WebUI_OLD.Models; using ILogger = DiscordBotCore.Logging.ILogger; -namespace WebUI.Controllers; +namespace WebUI_OLD.Controllers; public class PluginsController : Controller { diff --git a/WebUI/Controllers/SettingsController.cs b/WebUI_Old/Controllers/SettingsController.cs similarity index 97% rename from WebUI/Controllers/SettingsController.cs rename to WebUI_Old/Controllers/SettingsController.cs index 5caf36f..1e0fedc 100644 --- a/WebUI/Controllers/SettingsController.cs +++ b/WebUI_Old/Controllers/SettingsController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Mvc; -using WebUI.Models; +using WebUI_OLD.Models; using IConfiguration = DiscordBotCore.Configuration.IConfiguration; using ILogger = DiscordBotCore.Logging.ILogger; -namespace WebUI.Controllers; +namespace WebUI_OLD.Controllers; public class SettingsController : Controller { diff --git a/WebUI/Dockerfile b/WebUI_Old/Dockerfile similarity index 100% rename from WebUI/Dockerfile rename to WebUI_Old/Dockerfile diff --git a/WebUI/Models/ErrorViewModel.cs b/WebUI_Old/Models/ErrorViewModel.cs similarity index 83% rename from WebUI/Models/ErrorViewModel.cs rename to WebUI_Old/Models/ErrorViewModel.cs index 452f68e..59b546d 100644 --- a/WebUI/Models/ErrorViewModel.cs +++ b/WebUI_Old/Models/ErrorViewModel.cs @@ -1,4 +1,4 @@ -namespace WebUI.Models; +namespace WebUI_OLD.Models; public class ErrorViewModel { diff --git a/WebUI/Models/InstalledPluginViewModel.cs b/WebUI_Old/Models/InstalledPluginViewModel.cs similarity index 85% rename from WebUI/Models/InstalledPluginViewModel.cs rename to WebUI_Old/Models/InstalledPluginViewModel.cs index 51b318a..bcfbb18 100644 --- a/WebUI/Models/InstalledPluginViewModel.cs +++ b/WebUI_Old/Models/InstalledPluginViewModel.cs @@ -1,4 +1,4 @@ -namespace WebUI.Models; +namespace WebUI_OLD.Models; public class InstalledPluginViewModel { diff --git a/WebUI/Models/OnlinePluginViewModel.cs b/WebUI_Old/Models/OnlinePluginViewModel.cs similarity index 90% rename from WebUI/Models/OnlinePluginViewModel.cs rename to WebUI_Old/Models/OnlinePluginViewModel.cs index 16b919f..f0af1b3 100644 --- a/WebUI/Models/OnlinePluginViewModel.cs +++ b/WebUI_Old/Models/OnlinePluginViewModel.cs @@ -1,4 +1,4 @@ -namespace WebUI.Models; +namespace WebUI_OLD.Models; public class OnlinePluginViewModel { diff --git a/WebUI/Models/PluginDetailsViewModel.cs b/WebUI_Old/Models/PluginDetailsViewModel.cs similarity index 88% rename from WebUI/Models/PluginDetailsViewModel.cs rename to WebUI_Old/Models/PluginDetailsViewModel.cs index 2b2d44e..04cd972 100644 --- a/WebUI/Models/PluginDetailsViewModel.cs +++ b/WebUI_Old/Models/PluginDetailsViewModel.cs @@ -1,4 +1,4 @@ -namespace WebUI.Models; +namespace WebUI_OLD.Models; public class PluginDetailsViewModel { diff --git a/WebUI/Models/SettingsViewModel.cs b/WebUI_Old/Models/SettingsViewModel.cs similarity index 93% rename from WebUI/Models/SettingsViewModel.cs rename to WebUI_Old/Models/SettingsViewModel.cs index c7c8b8d..6e4daa5 100644 --- a/WebUI/Models/SettingsViewModel.cs +++ b/WebUI_Old/Models/SettingsViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace WebUI.Models; +namespace WebUI_OLD.Models; public class SettingsViewModel { diff --git a/WebUI_Old/Program.cs b/WebUI_Old/Program.cs new file mode 100644 index 0000000..adcc039 --- /dev/null +++ b/WebUI_Old/Program.cs @@ -0,0 +1,120 @@ +using System.Reflection; +using DiscordBotCore.Bot; +using DiscordBotCore.Configuration; +using DiscordBotCore.Logging; +using DiscordBotCore.PluginManagement; +using DiscordBotCore.PluginManagement.Helpers; +using DiscordBotCore.PluginManagement.Loading; +using DiscordBotCore.WebApplication; +using IConfiguration = DiscordBotCore.Configuration.IConfiguration; +using ILogger = DiscordBotCore.Logging.ILogger; + +#region Load External (Unmanaged) Assemblies + +// This code is used to load external (unmanaged) assemblies from the same folder as the executing assembly. +// It handles the AssemblyResolve event to search for the requested assembly in a specific folder structure. +// The folder structure is expected to be: /Libraries//. +// The extensions to search for are specified in the 'extensions' parameter. + +var currentDomain = AppDomain.CurrentDomain; +currentDomain.AssemblyResolve += (sender, args) => LoadFromSameFolder(sender,args, [".dll", ".so", ".dylib"]); + +static Assembly? LoadFromSameFolder(object? sender, ResolveEventArgs args, string[] extensions) +{ + string? requestingAssemblyName = args.RequestingAssembly?.GetName().Name; + string executingAssemblyLocation = Assembly.GetExecutingAssembly().Location; + string? executingAssemblyDirectory = Path.GetDirectoryName(executingAssemblyLocation); + + if (string.IsNullOrEmpty(executingAssemblyDirectory)) + { + Console.WriteLine($"Error: Could not determine the directory of the executing assembly."); + return null; + } + + string librariesFolder = Path.Combine(executingAssemblyDirectory, "Libraries", requestingAssemblyName ?? ""); + string requestedAssemblyNameWithoutExtension = new AssemblyName(args.Name).Name; + + Console.WriteLine($"Requesting Assembly: {requestingAssemblyName}"); + Console.WriteLine($"Requested Assembly Name (without extension): {requestedAssemblyNameWithoutExtension}"); + Console.WriteLine($"Searching in folder: {librariesFolder}"); + Console.WriteLine($"Searching for extensions: {string.Join(", ", extensions)}"); + + foreach (string extension in extensions) + { + string assemblyFileName = requestedAssemblyNameWithoutExtension + extension; + string assemblyPath = Path.Combine(librariesFolder, assemblyFileName); + + Console.WriteLine($"Attempting to load from: {assemblyPath}"); + + if (File.Exists(assemblyPath)) + { + try + { + var fileAssembly = Assembly.LoadFrom(assemblyPath); + Console.WriteLine($"Successfully loaded Assembly: {fileAssembly.FullName}"); + return fileAssembly; + } + catch (Exception ex) + { + Console.WriteLine($"Error loading assembly from '{assemblyPath}': {ex.Message}"); + // Optionally log the full exception for debugging + } + } + else + { + Console.WriteLine($"File not found: {assemblyPath}"); + } + } + + Console.WriteLine($"Failed to load assembly '{args.Name}' from the specified locations."); + return null; +} +#endregion + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllersWithViews(); +builder.AddDiscordBotComponents(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthorization(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + +// Force eager creation of all required services +using (var scope = app.Services.CreateScope()) +{ + var provider = scope.ServiceProvider; + + // Manually resolve all your singletons here + provider.GetRequiredService(); + IConfiguration config = provider.GetRequiredService(); + provider.GetRequiredService(); + provider.GetRequiredService(); + provider.GetRequiredService(); + provider.GetRequiredService(); + provider.GetRequiredService(); + + // Optional: Log that all services were initialized + provider.GetRequiredService().Log("All core services have been initialized at startup."); +} + + +app.Run(); \ No newline at end of file diff --git a/WebUI_Old/Properties/launchSettings.json b/WebUI_Old/Properties/launchSettings.json new file mode 100644 index 0000000..b2c3329 --- /dev/null +++ b/WebUI_Old/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52426", + "sslPort": 44369 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5111", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7222;http://localhost:5111", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebUI/Views/Home/Index.cshtml b/WebUI_Old/Views/Home/Index.cshtml similarity index 100% rename from WebUI/Views/Home/Index.cshtml rename to WebUI_Old/Views/Home/Index.cshtml diff --git a/WebUI/Views/Plugins/InstalledPlugins.cshtml b/WebUI_Old/Views/Plugins/InstalledPlugins.cshtml similarity index 100% rename from WebUI/Views/Plugins/InstalledPlugins.cshtml rename to WebUI_Old/Views/Plugins/InstalledPlugins.cshtml diff --git a/WebUI/Views/Plugins/OnlinePlugins.cshtml b/WebUI_Old/Views/Plugins/OnlinePlugins.cshtml similarity index 100% rename from WebUI/Views/Plugins/OnlinePlugins.cshtml rename to WebUI_Old/Views/Plugins/OnlinePlugins.cshtml diff --git a/WebUI/Views/Plugins/PluginDetails.cshtml b/WebUI_Old/Views/Plugins/PluginDetails.cshtml similarity index 100% rename from WebUI/Views/Plugins/PluginDetails.cshtml rename to WebUI_Old/Views/Plugins/PluginDetails.cshtml diff --git a/WebUI/Views/Settings/Index.cshtml b/WebUI_Old/Views/Settings/Index.cshtml similarity index 100% rename from WebUI/Views/Settings/Index.cshtml rename to WebUI_Old/Views/Settings/Index.cshtml diff --git a/WebUI/Views/Shared/Error.cshtml b/WebUI_Old/Views/Shared/Error.cshtml similarity index 100% rename from WebUI/Views/Shared/Error.cshtml rename to WebUI_Old/Views/Shared/Error.cshtml diff --git a/WebUI/Views/Shared/_Layout.cshtml b/WebUI_Old/Views/Shared/_Layout.cshtml similarity index 91% rename from WebUI/Views/Shared/_Layout.cshtml rename to WebUI_Old/Views/Shared/_Layout.cshtml index 7b464aa..9deabfd 100644 --- a/WebUI/Views/Shared/_Layout.cshtml +++ b/WebUI_Old/Views/Shared/_Layout.cshtml @@ -3,16 +3,16 @@ - @ViewData["Title"] - WebUI + @ViewData["Title"] - WebUI_OLD - +