diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index eb84b19..d0bab17 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Concurrent; using ChatTwo.Code; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; @@ -122,7 +123,10 @@ internal class Configuration : IPluginConfiguration // Webinterface public bool WebinterfaceEnabled; + public bool WebinterfaceAutoStart; public string WebinterfacePassword = WebinterfaceUtil.GenerateSimpleAuthCode(); + public int WebinterfacePort = 9000; + public ConcurrentDictionary SessionTokens = []; internal void UpdateFrom(Configuration other, bool backToOriginal) { @@ -139,7 +143,7 @@ internal class Configuration : IPluginConfiguration HideWhenInactive = other.HideWhenInactive; InactivityHideTimeout = other.InactivityHideTimeout; InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle; - InactivityHideChannels = other.InactivityHideChannels.ToDictionary(entry => entry.Key, entry => entry.Value); + InactivityHideChannels = other.InactivityHideChannels?.ToDictionary(entry => entry.Key, entry => entry.Value); InactivityHideExtraChatAll = other.InactivityHideExtraChatAll; InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet(); ShowHideButton = other.ShowHideButton; @@ -188,7 +192,9 @@ internal class Configuration : IPluginConfiguration ChatTabForward = other.ChatTabForward; ChatTabBackward = other.ChatTabBackward; WebinterfaceEnabled = other.WebinterfaceEnabled; + WebinterfaceAutoStart = other.WebinterfaceAutoStart; WebinterfacePassword = other.WebinterfacePassword; + WebinterfacePort = other.WebinterfacePort; } } diff --git a/ChatTwo/Http/HostContext.cs b/ChatTwo/Http/HostContext.cs new file mode 100644 index 0000000..a3c3f5e --- /dev/null +++ b/ChatTwo/Http/HostContext.cs @@ -0,0 +1,135 @@ +using WatsonWebserver.Core; +using WatsonWebserver.Lite; + +namespace ChatTwo.Http; + +public class HostContext +{ + private readonly Plugin Plugin; + + public bool IsActive; + public bool IsStopping; + + internal WebserverLite Host; + internal Processing Processing; + internal RouteController RouteController; + + internal readonly List EventConnections = []; + + internal readonly CancellationTokenSource TokenSource = new(); + internal readonly string StaticDir = Path.Combine(Plugin.Interface.AssemblyLocation.DirectoryName!, "Http"); + + public HostContext(Plugin plugin) + { + Plugin = plugin; + } + + public bool Start() + { + try + { + Host = new WebserverLite(new WebserverSettings("*", Plugin.Config.WebinterfacePort), DefaultRoute); + + Processing = new Processing(Plugin); + RouteController = new RouteController(Plugin, this); + + Host.Routes.PreAuthentication.Content.BaseDirectory = StaticDir; + Host.Routes.AuthenticateRequest = CheckAuthenticationCookie; + Host.Events.ExceptionEncountered += ExceptionEncountered; + + // Settings + Host.Settings.Debug.Requests = true; + Host.Settings.Debug.Routing = true; + Host.Settings.Debug.Responses = true; + Host.Settings.Debug.AccessControl = true; + Host.Events.Logger = logMessage => Plugin.Log.Information(logMessage); + + IsActive = true; + return true; + } + catch (Exception ex) + { + IsActive = false; + Plugin.Log.Error(ex, "Initialization of the webserver failed."); + return false; + } + } + + public void Run() + { + try + { + Host.Start(TokenSource.Token); + } + catch (Exception ex) + { + Plugin.Log.Error(ex, "Webserver failed to boot up."); + } + } + + public async ValueTask Stop() + { + // Is already stopped + if (!IsActive) + return true; + + try + { + IsActive = false; + IsStopping = true; + await Task.Delay(10000); + Host.Stop(); + + // Save our session tokens + Plugin.SaveConfig(); + + // We get a copy, so that the original can be cleaned up successfully + foreach (var eventServer in EventConnections.ToArray()) + await eventServer.DisposeAsync(); + + EventConnections.Clear(); + Host.Dispose(); + RouteController.Dispose(); + IsStopping = false; + + return true; + } + catch (Exception ex) + { + Plugin.Log.Error(ex, "Webserver failed to stop and dispose all resources."); + return false; + } + } + + public async ValueTask DisposeAsync() + { + await Stop(); + } + + #region GeneralHandlers + private static void ExceptionEncountered(object? _, ExceptionEventArgs args) + { + Plugin.Log.Error(args.Exception, "Webserver threw an exception."); + } + + private async Task DefaultRoute(HttpContextBase ctx) + { + await ctx.Response.Send("Nothing to see here."); + } + #endregion + + private async Task CheckAuthenticationCookie(HttpContextBase ctx) + { + if (Plugin.Config.SessionTokens.IsEmpty) + { + await RouteController.Redirect(ctx, "/", ("message", "Invalid session token.")); + return; + } + + var cookies = WebserverUtil.GetCookieData(ctx.Request.Headers.Get("Cookie") ?? ""); + if (!cookies.TryGetValue("ChatTwo-token", out var token) || !Plugin.Config.SessionTokens.ContainsKey(token)) + await RouteController.Redirect(ctx, "/", ("message", "Invalid session token.")); + + // Do nothing to let auth pass + } +} \ No newline at end of file diff --git a/ChatTwo/Http/RouteController.cs b/ChatTwo/Http/RouteController.cs index 722749e..c79c667 100644 --- a/ChatTwo/Http/RouteController.cs +++ b/ChatTwo/Http/RouteController.cs @@ -14,20 +14,19 @@ namespace ChatTwo.Http; public class RouteController { private readonly Plugin Plugin; - private readonly ServerCore Core; + private readonly HostContext Core; private readonly string AuthTemplate; private readonly string ChatBoxTemplate; private readonly ConcurrentDictionary RateLimit = []; - internal readonly ConcurrentDictionary SessionTokens = []; private readonly JsonSerializerSettings JsonSettings = new() { - Error = delegate(object? sender, ErrorEventArgs args) { args.ErrorContext.Handled = true; } + Error = delegate(object? _, ErrorEventArgs args) { args.ErrorContext.Handled = true; } }; - public RouteController(Plugin plugin, ServerCore core) + public RouteController(Plugin plugin, HostContext core) { Plugin = plugin; Core = core; @@ -36,21 +35,21 @@ public class RouteController ChatBoxTemplate = File.ReadAllText(Path.Combine(Core.StaticDir, "templates", "chat.html")); // Pre Auth - Core.HostContext.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/", AuthRoute, ExceptionRoute); - Core.HostContext.Routes.PreAuthentication.Static.Add(HttpMethod.POST, "/auth", AuthenticateClient, ExceptionRoute); - Core.HostContext.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/gfdata.gfd", GetGfdData, ExceptionRoute); - Core.HostContext.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/fonticon_ps5.tex", GetTexData, ExceptionRoute); - Core.HostContext.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/FFXIV_Lodestone_SSF.ttf", GetLodestoneFont, ExceptionRoute); - Core.HostContext.Routes.PreAuthentication.Parameter.Add(HttpMethod.GET, "/emote/{name}", GetEmote, ExceptionRoute); - Core.HostContext.Routes.PreAuthentication.Content.Add("/static", true, ExceptionRoute); + Core.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/", AuthRoute, ExceptionRoute); + Core.Host.Routes.PreAuthentication.Static.Add(HttpMethod.POST, "/auth", AuthenticateClient, ExceptionRoute); + Core.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/gfdata.gfd", GetGfdData, ExceptionRoute); + Core.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/fonticon_ps5.tex", GetTexData, ExceptionRoute); + Core.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/FFXIV_Lodestone_SSF.ttf", GetLodestoneFont, ExceptionRoute); + Core.Host.Routes.PreAuthentication.Parameter.Add(HttpMethod.GET, "/emote/{name}", GetEmote, ExceptionRoute); + Core.Host.Routes.PreAuthentication.Content.Add("/static", true, ExceptionRoute); // Post Auth - Core.HostContext.Routes.PostAuthentication.Static.Add(HttpMethod.GET, "/chat", ChatBoxRoute, ExceptionRoute); - Core.HostContext.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/send", ReceiveMessage, ExceptionRoute); - Core.HostContext.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/channel", ReceiveChannelSwitch, ExceptionRoute); + Core.Host.Routes.PostAuthentication.Static.Add(HttpMethod.GET, "/chat", ChatBoxRoute, ExceptionRoute); + Core.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/send", ReceiveMessage, ExceptionRoute); + Core.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/channel", ReceiveChannelSwitch, ExceptionRoute); // Server-Sent Events Route - Core.HostContext.Routes.PostAuthentication.Static.Add(HttpMethod.GET, "/sse", NewSSEConnection, ExceptionRoute); + Core.Host.Routes.PostAuthentication.Static.Add(HttpMethod.GET, "/sse", NewSSEConnection, ExceptionRoute); } private async Task ExceptionRoute(HttpContextBase ctx, Exception _) @@ -135,7 +134,7 @@ public class RouteController return await Redirect(ctx, "/", ("message", "Authentication failed.")); var token = WebinterfaceUtil.GenerateSimpleToken(); - SessionTokens.TryAdd(token, true); + Plugin.Config.SessionTokens.TryAdd(token, true); ctx.Response.Headers.Add("Set-Cookie", $"ChatTwo-token={token}"); return await Redirect(ctx, "/chat"); diff --git a/ChatTwo/Http/ServerCore.cs b/ChatTwo/Http/ServerCore.cs index 5c757ae..5f0218b 100644 --- a/ChatTwo/Http/ServerCore.cs +++ b/ChatTwo/Http/ServerCore.cs @@ -1,52 +1,29 @@ using ChatTwo.Http.MessageProtocol; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; -using ExceptionEventArgs = WatsonWebserver.Core.ExceptionEventArgs; - namespace ChatTwo.Http; public class ServerCore : IAsyncDisposable { private readonly Plugin Plugin; - internal readonly Processing Processing; - internal readonly RouteController RouteController; - - internal readonly WebserverLite HostContext; - - internal readonly CancellationTokenSource TokenSource = new(); - internal readonly string StaticDir = Path.Combine(Plugin.Interface.AssemblyLocation.DirectoryName!, "Http"); - - internal readonly List EventConnections = []; + private readonly HostContext HostContext; public ServerCore(Plugin plugin) { Plugin = plugin; - HostContext = new WebserverLite(new WebserverSettings("*", 9000), DefaultRoute); - - Processing = new Processing(plugin); - RouteController = new RouteController(plugin, this); - - HostContext.Routes.PreAuthentication.Content.BaseDirectory = StaticDir; - HostContext.Routes.AuthenticateRequest = CheckAuthenticationCookie; - HostContext.Events.ExceptionEncountered += ExceptionEncountered; - - // Settings - HostContext.Settings.Debug.Requests = true; - HostContext.Settings.Debug.Routing = true; - HostContext.Settings.Debug.Responses = true; - HostContext.Settings.Debug.AccessControl = true; - HostContext.Events.Logger = logMessage => Plugin.Log.Information(logMessage); + HostContext = new HostContext(plugin); } - #region SSEFunctions + #region SSE Helper internal void SendNewMessage(Message message) { + if (!HostContext.IsActive) + return; + try { Plugin.Framework.RunOnTick(() => { - var bundledResponse = new NewMessageEvent(new Messages([Processing.ReadMessageContent(message)])); - foreach (var eventServer in EventConnections) + var bundledResponse = new NewMessageEvent(new Messages([HostContext.Processing.ReadMessageContent(message)])); + foreach (var eventServer in HostContext.EventConnections) eventServer.OutboundQueue.Enqueue(bundledResponse); }); } @@ -58,12 +35,15 @@ public class ServerCore : IAsyncDisposable internal void SendBulkMessageList() { + if (!HostContext.IsActive) + return; + try { Plugin.Framework.RunOnTick(() => { - foreach (var eventServer in EventConnections) - eventServer.OutboundQueue.Enqueue(new BulkMessagesEvent(new Messages(Processing.ReadMessageList().Result))); + foreach (var eventServer in HostContext.EventConnections) + eventServer.OutboundQueue.Enqueue(new BulkMessagesEvent(new Messages(HostContext.Processing.ReadMessageList().Result))); }); } catch (Exception ex) @@ -74,12 +54,15 @@ public class ServerCore : IAsyncDisposable internal void SendChannelSwitch(Chunk[] channelName) { + if (!HostContext.IsActive) + return; + try { Plugin.Framework.RunOnTick(() => { - var bundledResponse = new SwitchChannelEvent(new SwitchChannel(Processing.ReadChannelName(channelName))); - foreach (var eventServer in EventConnections) + var bundledResponse = new SwitchChannelEvent(new SwitchChannel(HostContext.Processing.ReadChannelName(channelName))); + foreach (var eventServer in HostContext.EventConnections) eventServer.OutboundQueue.Enqueue(bundledResponse); }); } @@ -91,13 +74,16 @@ public class ServerCore : IAsyncDisposable internal void SendChannelList() { + if (!HostContext.IsActive) + return; + try { Plugin.Framework.RunOnTick(() => { var channels = Plugin.ChatLogWindow.GetAvailableChannels(); var bundledResponse = new ChannelListEvent(new ChannelList(channels.ToDictionary(pair => pair.Key, pair => (uint)pair.Value))); - foreach (var eventServer in EventConnections) + foreach (var eventServer in HostContext.EventConnections) eventServer.OutboundQueue.Enqueue(bundledResponse); }); } @@ -108,65 +94,43 @@ public class ServerCore : IAsyncDisposable } #endregion - #region GeneralHandlers - private static void ExceptionEncountered(object? _, ExceptionEventArgs args) - { - Plugin.Log.Error(args.Exception, "Webserver threw an exception."); - } - - private async Task DefaultRoute(HttpContextBase ctx) - { - await ctx.Response.Send("Nothing to see here."); - } - #endregion - - private async Task CheckAuthenticationCookie(HttpContextBase ctx) - { - if (RouteController.SessionTokens.IsEmpty) - { - await RouteController.Redirect(ctx, "/", ("message", "Invalid session token.")); - return; - } - - var cookies = WebserverUtil.GetCookieData(ctx.Request.Headers.Get("Cookie") ?? ""); - if (!cookies.TryGetValue("ChatTwo-token", out var token) || !RouteController.SessionTokens.ContainsKey(token)) - await RouteController.Redirect(ctx, "/", ("message", "Invalid session token.")); - - // Do nothing to let auth pass - } - public void InvalidateSessions() { - RouteController.SessionTokens.Clear(); + if (!HostContext.IsActive) + return; + + Plugin.Config.SessionTokens.Clear(); + Plugin.SaveConfig(); } - public bool GetStats() + public bool IsActive() { - return HostContext.IsListening; + return HostContext is { IsActive: true, Host.IsListening: true }; } - public void Start() + public bool IsStopping() { - try - { - HostContext.Start(TokenSource.Token); - } - catch (Exception ex) - { - Plugin.Log.Error(ex, "Startup failed with an error."); - } + return HostContext is { IsActive: false, IsStopping: true }; + } + + + public bool Start() + { + return HostContext.Start(); + } + + public void Run() + { + HostContext.Run(); + } + + public async ValueTask Stop() + { + return await HostContext.Stop(); } public async ValueTask DisposeAsync() { - await TokenSource.CancelAsync(); - HostContext.Stop(); - - // We get a copy, so that the original can be cleaned up succesfully - foreach (var eventServer in EventConnections.ToArray()) - await eventServer.DisposeAsync(); - - HostContext.Dispose(); - RouteController.Dispose(); + await HostContext.DisposeAsync(); } } \ No newline at end of file diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index d37747d..a456a85 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -131,7 +131,16 @@ public sealed class Plugin : IDalamudPlugin #endif ServerCore = new ServerCore(this); - Task.Run(() => ServerCore.Start()); + + // Automatically start the webserver if requested + if (Config.WebinterfaceAutoStart) + { + Task.Run(() => + { + ServerCore.Start(); + ServerCore.Run(); + }); + } } catch (Exception ex) { diff --git a/ChatTwo/Resources/Language.Designer.cs b/ChatTwo/Resources/Language.Designer.cs index 4ce28a0..76db480 100755 --- a/ChatTwo/Resources/Language.Designer.cs +++ b/ChatTwo/Resources/Language.Designer.cs @@ -3416,6 +3416,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to The Webinterface is going to be started when the plugins loads.. + /// + internal static string Options_WebinterfaceAutoStart_Description { + get { + return ResourceManager.GetString("Options_WebinterfaceAutoStart_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Start automatically. + /// + internal static string Options_WebinterfaceAutoStart_Name { + get { + return ResourceManager.GetString("Options_WebinterfaceAutoStart_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Enables the webinterface that can be accessed with a browser.. /// @@ -3426,7 +3444,7 @@ namespace ChatTwo.Resources { } /// - /// Looks up a localized string similar to Enable. + /// Looks up a localized string similar to Enabled. /// internal static string Options_WebinterfaceEnable_Name { get { @@ -3570,7 +3588,43 @@ namespace ChatTwo.Resources { } /// - /// Looks up a localized string similar to Your current password to access the webinterface:. + /// Looks up a localized string similar to Start. + /// + internal static string Webinterface_Button_Start { + get { + return ResourceManager.GetString("Webinterface_Button_Start", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop. + /// + internal static string Webinterface_Button_Stop { + get { + return ResourceManager.GetString("Webinterface_Button_Stop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Controls:. + /// + internal static string Webinterface_Controls { + get { + return ResourceManager.GetString("Webinterface_Controls", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Active:. + /// + internal static string Webinterface_Controls_Active { + get { + return ResourceManager.GetString("Webinterface_Controls_Active", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Authcode:. /// internal static string Webinterface_CurrentPassword { get { @@ -3578,6 +3632,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to The port this webinterface will be running on.. + /// + internal static string Webinterface_Option_Port_Description { + get { + return ResourceManager.GetString("Webinterface_Option_Port_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Port. + /// + internal static string Webinterface_Option_Port_Name { + get { + return ResourceManager.GetString("Webinterface_Option_Port_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Reset your password and invalidate all session tokens.. /// @@ -3588,20 +3660,38 @@ namespace ChatTwo.Resources { } /// - /// Looks up a localized string similar to Webinterface Status:. + /// Looks up a localized string similar to Webinterface failed to start. Check /xllog for more information.. /// - internal static string Webinterface_Status { + internal static string Webinterface_Start_Failed { get { - return ResourceManager.GetString("Webinterface_Status", resourceCulture); + return ResourceManager.GetString("Webinterface_Start_Failed", resourceCulture); } } /// - /// Looks up a localized string similar to Active:. + /// Looks up a localized string similar to Webinterface was started.. /// - internal static string Webinterface_Status_Active { + internal static string Webinterface_Start_Success { get { - return ResourceManager.GetString("Webinterface_Status_Active", resourceCulture); + return ResourceManager.GetString("Webinterface_Start_Success", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Webinterface failed to stop. Check /xllog for more information.. + /// + internal static string Webinterface_Stop_Failed { + get { + return ResourceManager.GetString("Webinterface_Stop_Failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Webinterface was stopped.. + /// + internal static string Webinterface_Stop_Success { + get { + return ResourceManager.GetString("Webinterface_Stop_Success", resourceCulture); } } } diff --git a/ChatTwo/Resources/Language.resx b/ChatTwo/Resources/Language.resx index 492fa46..24bd86a 100644 --- a/ChatTwo/Resources/Language.resx +++ b/ChatTwo/Resources/Language.resx @@ -551,11 +551,17 @@ If this is enabled, the Auto Translate list will be sorted alphabetically. - Enable + Enabled Enables the webinterface that can be accessed with a browser. + + Start automatically + + + The Webinterface is going to be started when the plugins loads. + Override Style @@ -569,15 +575,15 @@ Cycle chat tab backwards keybind - Your current password to access the webinterface: + Authcode: Reset your password and invalidate all session tokens. - - Webinterface Status: + + Controls: - + Active: @@ -1303,4 +1309,28 @@ An error occured while sending this tell message + + Webinterface was stopped. + + + Webinterface failed to stop. Check /xllog for more information. + + + Stop + + + Start + + + Webinterface was started. + + + Webinterface failed to start. Check /xllog for more information. + + + Port + + + The port this webinterface will be running on. + diff --git a/ChatTwo/Ui/SettingsTabs/Webinterface.cs b/ChatTwo/Ui/SettingsTabs/Webinterface.cs index 536c4ba..6ee978f 100644 --- a/ChatTwo/Ui/SettingsTabs/Webinterface.cs +++ b/ChatTwo/Ui/SettingsTabs/Webinterface.cs @@ -2,6 +2,7 @@ using ChatTwo.Resources; using ChatTwo.Util; using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; using ImGuiNET; @@ -37,8 +38,7 @@ internal sealed class Webinterface(Plugin plugin, Configuration mutable) : ISett ImGui.Separator(); ImGui.Spacing(); - ImGui.Checkbox(Language.Options_WebinterfaceEnable_Name, ref Mutable.WebinterfaceEnabled); - ImGuiUtil.HelpText(Language.Options_WebinterfaceEnable_Description); + ImGuiUtil.OptionCheckbox(ref Mutable.WebinterfaceEnabled, Language.Options_WebinterfaceEnable_Name, Language.Options_WebinterfaceEnable_Description); ImGui.Spacing(); if (!Mutable.WebinterfaceEnabled) @@ -47,6 +47,13 @@ internal sealed class Webinterface(Plugin plugin, Configuration mutable) : ISett ImGui.Separator(); ImGui.Spacing(); + ImGuiUtil.OptionCheckbox(ref Mutable.WebinterfaceAutoStart, Language.Options_WebinterfaceAutoStart_Name, Language.Options_WebinterfaceAutoStart_Description); + ImGui.Spacing(); + + if (ImGuiUtil.InputIntVertical(Language.Webinterface_Option_Port_Name, Language.Webinterface_Option_Port_Description, ref Mutable.WebinterfacePort)) + Mutable.WebinterfacePort = Math.Clamp(Mutable.WebinterfacePort, 1024, 49151); + ImGui.Spacing(); + ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudOrange, Language.Webinterface_CurrentPassword); ImGui.TextUnformatted(Mutable.WebinterfacePassword); ImGui.SameLine(); @@ -56,18 +63,56 @@ internal sealed class Webinterface(Plugin plugin, Configuration mutable) : ISett Plugin.ServerCore.InvalidateSessions(); } - ImGuiUtil.WrappedTextWithColor(ImGuiColors.HealerGreen, Language.Webinterface_Status); + ImGuiUtil.WrappedTextWithColor(ImGuiColors.HealerGreen, Language.Webinterface_Controls); using (ImRaii.PushIndent(10.0f)) { - ImGui.TextUnformatted(Language.Webinterface_Status_Active); + ImGui.TextUnformatted(Language.Webinterface_Controls_Active); ImGui.SameLine(); - var isActive = Plugin.ServerCore.GetStats(); + var isActive = Plugin.ServerCore.IsActive(); using (Plugin.FontManager.FontAwesome.Push()) - using (ImRaii.PushColor(ImGuiCol.Text, isActive ? ImGuiColors.HealerGreen : ImGuiColors.DalamudOrange)) + using (ImRaii.PushColor(ImGuiCol.Text, isActive ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed)) { - ImGui.TextUnformatted($"{(isActive ? FontAwesomeIcon.Check.ToIconString() : FontAwesomeIcon.Cross.ToIconString())}"); + ImGui.TextUnformatted(isActive ? FontAwesomeIcon.Check.ToIconString() : FontAwesomeIcon.Times.ToIconString()); + } + + using (ImRaii.Disabled(isActive || Plugin.ServerCore.IsStopping())) + { + if (ImGui.Button(Language.Webinterface_Button_Start)) + { + Task.Run(() => + { + var ok = Plugin.ServerCore.Start(); + if (ok) + { + Plugin.ServerCore.Run(); + WrapperUtil.AddNotification(Language.Webinterface_Start_Success, NotificationType.Success); + } + else + { + WrapperUtil.AddNotification(Language.Webinterface_Start_Failed, NotificationType.Error); + } + }); + } + } + + ImGui.SameLine(); + + using (ImRaii.Disabled(!isActive || Plugin.ServerCore.IsStopping())) + { + if (ImGui.Button(Language.Webinterface_Button_Stop)) + { + Task.Run(async () => + { + var ok = await Plugin.ServerCore.Stop(); + if (ok) + WrapperUtil.AddNotification(Language.Webinterface_Stop_Success, NotificationType.Success); + else + WrapperUtil.AddNotification(Language.Webinterface_Stop_Failed, NotificationType.Error); + }); + } } } + ImGui.Spacing(); } }