Add pre-testing version of the webinterface
This commit is contained in:
@@ -46,14 +46,21 @@
|
||||
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(DalamudLibPath)\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.13" />
|
||||
<PackageReference Include="EmbedIO" Version="3.5.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.1.870" />
|
||||
<PackageReference Include="MessagePack" Version="2.5.140" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.4" />
|
||||
<PackageReference Include="Pidgin" Version="3.2.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageReference Include="Watson.Lite" Version="6.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -74,4 +81,13 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="images\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Http\static\*.*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Http\templates\*.*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+48
-26
@@ -120,6 +120,10 @@ internal class Configuration : IPluginConfiguration
|
||||
public ConfigKeyBind? ChatTabForward;
|
||||
public ConfigKeyBind? ChatTabBackward;
|
||||
|
||||
// Webinterface
|
||||
public bool WebinterfaceEnabled;
|
||||
public string WebinterfacePassword = WebinterfaceUtil.GenerateSimpleAuthCode();
|
||||
|
||||
internal void UpdateFrom(Configuration other, bool backToOriginal)
|
||||
{
|
||||
if (backToOriginal)
|
||||
@@ -183,6 +187,8 @@ internal class Configuration : IPluginConfiguration
|
||||
ChosenStyle = other.ChosenStyle;
|
||||
ChatTabForward = other.ChatTabForward;
|
||||
ChatTabBackward = other.ChatTabBackward;
|
||||
WebinterfaceEnabled = other.WebinterfaceEnabled;
|
||||
WebinterfacePassword = other.WebinterfacePassword;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,26 +292,26 @@ internal class Tab
|
||||
/// </summary>
|
||||
internal class MessageList
|
||||
{
|
||||
private ReaderWriterLock rwl = new();
|
||||
private readonly SemaphoreSlim LockSlim = new(1, 1);
|
||||
|
||||
private readonly List<Message> messages;
|
||||
private readonly HashSet<Guid> trackedMessageIds;
|
||||
private readonly List<Message> Messages;
|
||||
private readonly HashSet<Guid> TrackedMessageIds;
|
||||
|
||||
public MessageList()
|
||||
{
|
||||
messages = new();
|
||||
trackedMessageIds = new();
|
||||
Messages = [];
|
||||
TrackedMessageIds = [];
|
||||
}
|
||||
|
||||
public MessageList(int initialCapacity)
|
||||
{
|
||||
messages = new(initialCapacity);
|
||||
trackedMessageIds = new(initialCapacity);
|
||||
Messages = new List<Message>(initialCapacity);
|
||||
TrackedMessageIds = new HashSet<Guid>(initialCapacity);
|
||||
}
|
||||
|
||||
public void AddPrune(Message message, int max)
|
||||
{
|
||||
rwl.AcquireWriterLock(-1);
|
||||
LockSlim.Wait(-1);
|
||||
try
|
||||
{
|
||||
AddLocked(message);
|
||||
@@ -313,13 +319,13 @@ internal class Tab
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwl.ReleaseWriterLock();
|
||||
LockSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSortPrune(IEnumerable<Message> messages, int max)
|
||||
{
|
||||
rwl.AcquireWriterLock(-1);
|
||||
LockSlim.Wait(-1);
|
||||
try
|
||||
{
|
||||
foreach (var message in messages)
|
||||
@@ -330,44 +336,60 @@ internal class Tab
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwl.ReleaseWriterLock();
|
||||
LockSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddLocked(Message message)
|
||||
{
|
||||
if (trackedMessageIds.Contains(message.Id))
|
||||
if (TrackedMessageIds.Contains(message.Id))
|
||||
return;
|
||||
|
||||
messages.Add(message);
|
||||
trackedMessageIds.Add(message.Id);
|
||||
Messages.Add(message);
|
||||
TrackedMessageIds.Add(message.Id);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
rwl.AcquireWriterLock(-1);
|
||||
LockSlim.Wait(-1);
|
||||
try
|
||||
{
|
||||
messages.Clear();
|
||||
trackedMessageIds.Clear();
|
||||
Messages.Clear();
|
||||
TrackedMessageIds.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwl.ReleaseWriterLock();
|
||||
LockSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void SortLocked()
|
||||
{
|
||||
messages.Sort((a, b) => a.Date.CompareTo(b.Date));
|
||||
Messages.Sort((a, b) => a.Date.CompareTo(b.Date));
|
||||
}
|
||||
|
||||
private void PruneMaxLocked(int max)
|
||||
{
|
||||
while (messages.Count > max)
|
||||
while (Messages.Count > max)
|
||||
{
|
||||
trackedMessageIds.Remove(messages[0].Id);
|
||||
messages.RemoveAt(0);
|
||||
TrackedMessageIds.Remove(Messages[0].Id);
|
||||
Messages.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array copy of the message list for usage outside of main thread
|
||||
/// </summary>
|
||||
public async Task<Message[]> GetCopy(int millisecondsTimeout = -1)
|
||||
{
|
||||
await LockSlim.WaitAsync(millisecondsTimeout);
|
||||
try
|
||||
{
|
||||
return Messages.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
LockSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,11 +399,11 @@ internal class Tab
|
||||
/// </summary>
|
||||
public RLockedMessageList GetReadOnly(int millisecondsTimeout = -1)
|
||||
{
|
||||
rwl.AcquireReaderLock(millisecondsTimeout);
|
||||
return new RLockedMessageList(rwl, messages);
|
||||
LockSlim.Wait(millisecondsTimeout);
|
||||
return new RLockedMessageList(LockSlim, Messages);
|
||||
}
|
||||
|
||||
internal class RLockedMessageList(ReaderWriterLock rwl, List<Message> messages) : IReadOnlyList<Message>, IDisposable
|
||||
internal class RLockedMessageList(SemaphoreSlim lockSlim, List<Message> messages) : IReadOnlyList<Message>, IDisposable
|
||||
{
|
||||
public IEnumerator<Message> GetEnumerator()
|
||||
{
|
||||
@@ -399,7 +421,7 @@ internal class Tab
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
rwl.ReleaseReaderLock();
|
||||
lockSlim.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-18
@@ -148,6 +148,8 @@ public static class EmoteCache
|
||||
public bool Failed;
|
||||
public bool IsLoaded;
|
||||
|
||||
public byte[] RawData = [];
|
||||
|
||||
protected IDalamudTextureWrap? Texture;
|
||||
|
||||
public virtual void Draw(Vector2 size)
|
||||
@@ -155,39 +157,26 @@ public static class EmoteCache
|
||||
ImGui.Image(Texture!.ImGuiHandle, size);
|
||||
}
|
||||
|
||||
internal static async Task<byte[]> LoadAsync(Emote emote)
|
||||
internal async Task<byte[]> LoadAsync(Emote emote)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Remove after 01.06.2024
|
||||
var oldDir = Path.Join(Plugin.Interface.ConfigDirectory.FullName, "emotes");
|
||||
if (Directory.Exists(oldDir))
|
||||
Directory.Delete(oldDir, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
var dir = Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1");
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
byte[] image;
|
||||
var filePath = Path.Join(dir, $"{emote.Id}.{emote.ImageType}");
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
image = await File.ReadAllBytesAsync(filePath);
|
||||
RawData = await File.ReadAllBytesAsync(filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var content = await new HttpClient().GetAsync(EmotePath.Format(emote.Id));
|
||||
image = await content.Content.ReadAsByteArrayAsync();
|
||||
RawData = await content.Content.ReadAsByteArrayAsync();
|
||||
|
||||
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
stream.Write(image, 0, image.Length);
|
||||
stream.Write(RawData, 0, RawData.Length);
|
||||
}
|
||||
|
||||
return image;
|
||||
return RawData;
|
||||
}
|
||||
|
||||
public abstract void InnerDispose();
|
||||
|
||||
@@ -16,7 +16,7 @@ public class FontManager
|
||||
|
||||
internal IFontHandle FontAwesome { get; private set; }
|
||||
|
||||
private readonly byte[] GameSymFont;
|
||||
internal readonly byte[] GameSymFont;
|
||||
|
||||
private ushort[] Ranges;
|
||||
private ushort[] JpRange;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ChatTwo.Http.MessageProtocol;
|
||||
|
||||
public struct MessageResponse()
|
||||
{
|
||||
[JsonProperty("timestamp")] public string Timestamp = "";
|
||||
[JsonProperty("messageHTML")] public string Message = "";
|
||||
}
|
||||
|
||||
public class WebSocketNewMessage(MessageResponse[] messages) : BaseOutboundMessage(MessageName)
|
||||
{
|
||||
private const string MessageName = "chat-message";
|
||||
|
||||
[JsonProperty("messages")] public MessageResponse[] Messages { get; set; } = messages;
|
||||
}
|
||||
|
||||
public class BaseOutboundMessage(string messageType)
|
||||
{
|
||||
[JsonProperty("messageType")] public string MessageType { get; set; } = messageType;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Globalization;
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Ganss.Xss;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class Processing
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
private readonly HtmlSanitizer Sanitizer = new();
|
||||
|
||||
public Processing(Plugin plugin)
|
||||
{
|
||||
Plugin = plugin;
|
||||
}
|
||||
|
||||
public string ReadChannelName()
|
||||
{
|
||||
var messages = new List<string>();
|
||||
foreach (var chunk in Plugin.ChatLogWindow.ReadChannelName(Plugin.ChatLogWindow.CurrentTab))
|
||||
messages.Add(ProcessChunk(chunk, noColor: true));
|
||||
|
||||
return string.Join("", messages);
|
||||
}
|
||||
|
||||
internal async Task<List<MessageResponse>> ReadMessageList()
|
||||
{
|
||||
var tabMessages = await Plugin.ChatLogWindow.CurrentTab!.Messages.GetCopy();
|
||||
return tabMessages.Select(ReadMessageContent).ToList();
|
||||
}
|
||||
|
||||
internal MessageResponse ReadMessageContent(Message message)
|
||||
{
|
||||
var response = new MessageResponse
|
||||
{
|
||||
Timestamp = message.Date.ToLocalTime().ToString("t", !Plugin.Config.Use24HourClock ? null : CultureInfo.CreateSpecificCulture("es-ES"))
|
||||
};
|
||||
|
||||
var content = "";
|
||||
if (message.Sender.Count > 0)
|
||||
content = message.Sender.Aggregate(content, (current, chunk) => current + ProcessChunk(chunk));
|
||||
|
||||
content = message.Content.Aggregate(content, (current, chunk) => current + ProcessChunk(chunk));
|
||||
response.Message = content;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private string ProcessChunk(Chunk chunk, bool noColor = false)
|
||||
{
|
||||
if (chunk is IconChunk { } icon)
|
||||
{
|
||||
return IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out _)
|
||||
? $"<span class=\"gfd-icon gfd-icon-hq-{(uint)icon.Icon}\" style=\"zoom:calc(16 * 4 / 3 / 40 * 1.4)\"></span>"
|
||||
: "";
|
||||
}
|
||||
|
||||
if (chunk is TextChunk { } text)
|
||||
{
|
||||
if (chunk.Link is EmotePayload emotePayload && Plugin.Config.ShowEmotes)
|
||||
{
|
||||
var image = EmoteCache.GetEmote(emotePayload.Code);
|
||||
if (image is { Failed: false })
|
||||
return $"<span style\"height: 1em\"><img src=\"/emote/{emotePayload.Code}\"></span>";
|
||||
}
|
||||
|
||||
var colour = text.Foreground;
|
||||
if (colour == null && text.FallbackColour != null)
|
||||
{
|
||||
var type = text.FallbackColour.Value;
|
||||
colour = Plugin.Config.ChatColours.TryGetValue(type, out var col) ? col : type.DefaultColor();
|
||||
}
|
||||
|
||||
var color = ColourUtil.RgbaToComponents(colour ?? 0);
|
||||
|
||||
var userContent = text.Content ?? "";
|
||||
if (Plugin.ChatLogWindow.ScreenshotMode)
|
||||
{
|
||||
if (chunk.Link is PlayerPayload playerPayload)
|
||||
userContent = Plugin.ChatLogWindow.HidePlayerInString(userContent, playerPayload.PlayerName, playerPayload.World.RowId);
|
||||
else if (Plugin.ClientState.LocalPlayer is { } player)
|
||||
userContent = Plugin.ChatLogWindow.HidePlayerInString(userContent, player.Name.TextValue, player.HomeWorld.Id);
|
||||
}
|
||||
|
||||
userContent = Sanitizer.Sanitize(userContent);
|
||||
return noColor
|
||||
? userContent
|
||||
: $"<span style=\"color:rgba({color.r}, {color.g}, {color.b}, {color.a})\">{userContent}</span>";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using Lumina.Data.Files;
|
||||
using WatsonWebserver.Core;
|
||||
|
||||
using HttpMethod = WatsonWebserver.Core.HttpMethod;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class RouteController
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
private readonly ServerCore Core;
|
||||
|
||||
private readonly string AuthTemplate;
|
||||
private readonly string ChatBoxTemplate;
|
||||
|
||||
public RouteController(Plugin plugin, ServerCore core)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Core = core;
|
||||
|
||||
AuthTemplate = File.ReadAllText(Path.Combine(Core.StaticDir, "templates", "auth.html"));
|
||||
ChatBoxTemplate = File.ReadAllText(Path.Combine(Core.StaticDir, "templates", "start.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.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.Parameter.Add(HttpMethod.GET, "/emote/{name}", GetEmote, ExceptionRoute);
|
||||
}
|
||||
|
||||
private async Task ExceptionRoute(HttpContextBase ctx, Exception _)
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
await ctx.Response.Send("Internal Server Error, please try again");
|
||||
}
|
||||
|
||||
private async Task AuthRoute(HttpContextBase ctx)
|
||||
{
|
||||
await ctx.Response.Send(AuthTemplate);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#region FileHandlerRoutes
|
||||
private async Task GetTexData(HttpContextBase ctx)
|
||||
{
|
||||
var data = Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!.Data;
|
||||
await ctx.Response.Send(data);
|
||||
}
|
||||
|
||||
private async Task GetGfdData(HttpContextBase ctx)
|
||||
{
|
||||
var data = Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data;
|
||||
await ctx.Response.Send(data);
|
||||
}
|
||||
|
||||
private async Task GetLodestoneFont(HttpContextBase ctx)
|
||||
{
|
||||
var data = Plugin.FontManager.GameSymFont;
|
||||
await ctx.Response.Send(data);
|
||||
}
|
||||
|
||||
private async Task GetEmote(HttpContextBase ctx)
|
||||
{
|
||||
var name = ctx.Request.Url.Parameters["name"] ?? "";
|
||||
if (name == "" || !EmoteCache.Exists(name))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send("Malformed emote name.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var emote = EmoteCache.GetEmote(name);
|
||||
if (emote is not { IsLoaded: true })
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send("Emote not valid.");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Response.Headers.Add("Cache-Control", "max-age=86400");
|
||||
await ctx.Response.Send(emote.RawData);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PreAuthRoutes
|
||||
private async Task AuthenticateClient(HttpContextBase ctx)
|
||||
{
|
||||
var receivedPassword = ctx.Request.DataAsString ?? "";
|
||||
if (!receivedPassword.StartsWith("authcode="))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.Send("Authentication content invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
receivedPassword = receivedPassword[9..];
|
||||
if (receivedPassword != Plugin.Config.WebinterfacePassword)
|
||||
{
|
||||
ctx.Response.StatusCode = 401;
|
||||
await ctx.Response.Send("Authentication failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Response.Headers.Add("Set-Cookie", $"auth={Plugin.Config.WebinterfacePassword}");
|
||||
ctx.Response.Headers.Add("Location", "/chat");
|
||||
ctx.Response.StatusCode = 302;
|
||||
await ctx.Response.Send();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PostAuthRoutes
|
||||
private async Task ChatBoxRoute(HttpContextBase ctx)
|
||||
{
|
||||
if (Plugin.ChatLogWindow.CurrentTab == null)
|
||||
{
|
||||
await ctx.Response.Send("No valid chat tab!");
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Response.Send(ChatBoxTemplate);
|
||||
}
|
||||
|
||||
private async Task ReceiveMessage(HttpContextBase ctx)
|
||||
{
|
||||
var content = ctx.Request.DataAsString;
|
||||
if (content.Length is > 500 or < 2)
|
||||
{
|
||||
await ctx.Response.Send("Invalid length for a chat message received.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Plugin.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
Plugin.ChatLogWindow.Chat = content;
|
||||
Plugin.ChatLogWindow.SendChatBox(Plugin.ChatLogWindow.CurrentTab);
|
||||
});
|
||||
|
||||
await ctx.Response.Send("Message was send to the channel.");
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using EmbedIO;
|
||||
using WatsonWebserver.Core;
|
||||
using WatsonWebserver.Lite;
|
||||
using ExceptionEventArgs = WatsonWebserver.Core.ExceptionEventArgs;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class ServerCore : IDisposable
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
private readonly Processing Processing;
|
||||
private readonly RouteController RouteController;
|
||||
|
||||
internal readonly WebserverLite HostContext;
|
||||
private readonly WebSocketServer Websocket;
|
||||
private readonly WebServer Host;
|
||||
|
||||
internal readonly CancellationTokenSource TokenSource = new();
|
||||
internal readonly string StaticDir = Path.Combine(Plugin.Interface.AssemblyLocation.DirectoryName!, "Http");
|
||||
|
||||
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);
|
||||
|
||||
|
||||
// Websocket
|
||||
Host = new WebServer(o => o
|
||||
.WithUrlPrefixes($"http://*:9001")
|
||||
.WithMode(HttpListenerMode.EmbedIO)
|
||||
);
|
||||
|
||||
Websocket = new WebSocketServer("/ws");
|
||||
Host.WithModule(Websocket);
|
||||
|
||||
Websocket.OnClientConnected += ClientConnected;
|
||||
}
|
||||
|
||||
#region WebsocketFunctions
|
||||
private void ClientConnected(object? sender, EventArgs args)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var messages = await WebserverUtil.FrameworkWrapper(Processing.ReadMessageList);
|
||||
Websocket.BroadcastMessage(new WebSocketNewMessage(messages.ToArray()));
|
||||
});
|
||||
}
|
||||
|
||||
internal void SendNewMessage(Message message)
|
||||
{
|
||||
try
|
||||
{
|
||||
Plugin.Framework.RunOnTick(() =>
|
||||
{
|
||||
Websocket.BroadcastMessage(new WebSocketNewMessage([Processing.ReadMessageContent(message)]));
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Send message to websockets failed.");
|
||||
}
|
||||
}
|
||||
#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)
|
||||
{
|
||||
var cookie = ctx.Request.Headers.Get("Cookie") ?? "";
|
||||
if (!cookie.StartsWith("auth=") || cookie[5..] != Plugin.Config.WebinterfacePassword)
|
||||
{
|
||||
ctx.Response.StatusCode = 401;
|
||||
await ctx.Response.Send("Your session auth code was invalid");
|
||||
}
|
||||
|
||||
// Do nothing to let auth pass
|
||||
}
|
||||
|
||||
public bool GetStats()
|
||||
{
|
||||
return HostContext.IsListening;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
HostContext.Start(TokenSource.Token);
|
||||
Host.Start(TokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Startup failed with an error.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TokenSource.Cancel();
|
||||
|
||||
HostContext.Stop();
|
||||
HostContext.Dispose();
|
||||
|
||||
Websocket.Dispose();
|
||||
Host.Dispose();
|
||||
|
||||
RouteController.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public static class WebserverUtil
|
||||
{
|
||||
public static async Task<T> FrameworkWrapper<T>(Func<Task<T>> func)
|
||||
{
|
||||
return await Plugin.Framework.RunOnTick(func).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public class DisposableWrapper : IDisposable {
|
||||
private readonly Action Down;
|
||||
private bool Disposed;
|
||||
|
||||
public DisposableWrapper(Action down) {
|
||||
Down = down;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (Disposed) return;
|
||||
|
||||
Down();
|
||||
Disposed = true;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AsyncUtils {
|
||||
public static async Task<IDisposable> UseWaitAsync(this SemaphoreSlim semaphore, CancellationToken ct = default) {
|
||||
await semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
|
||||
return new WebserverUtil.DisposableWrapper(() => {
|
||||
semaphore.Release();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using ChatTwo.Http.MessageProtocol;
|
||||
using EmbedIO.WebSockets;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ChatTwo.Http;
|
||||
|
||||
public class WebSocketServer : WebSocketModule {
|
||||
private readonly SemaphoreSlim SendLock = new(1, 1);
|
||||
|
||||
public event EventHandler? OnClientConnected;
|
||||
|
||||
public WebSocketServer(string urlPath) : base(urlPath, true) {
|
||||
|
||||
}
|
||||
|
||||
protected override async Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result)
|
||||
{
|
||||
// Unused method
|
||||
}
|
||||
|
||||
protected override Task OnClientConnectedAsync(IWebSocketContext context)
|
||||
{
|
||||
Plugin.Log.Information($"Client connected: {context.Id}");
|
||||
OnClientConnected?.Invoke(this, EventArgs.Empty);
|
||||
return base.OnClientConnectedAsync(context);
|
||||
}
|
||||
|
||||
protected override Task OnClientDisconnectedAsync(IWebSocketContext context)
|
||||
{
|
||||
Plugin.Log.Information($"Client disconnected: {context.Id}");
|
||||
return base.OnClientConnectedAsync(context);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
base.Dispose(disposing);
|
||||
|
||||
SendLock.Dispose();
|
||||
}
|
||||
|
||||
public void BroadcastMessage(BaseOutboundMessage message) {
|
||||
Task.Run(async () => {
|
||||
using (await SendLock.UseWaitAsync()) {
|
||||
var serializedData = JsonConvert.SerializeObject(message);
|
||||
await BroadcastAsync(serializedData);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,243 @@
|
||||
/* fonts */
|
||||
@font-face {
|
||||
font-family: Lodestone;
|
||||
src: url('files/FFXIV_Lodestone_SSF.ttf') format('truetype');
|
||||
unicode-range: U+E020-E0DB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter var';
|
||||
font-weight: 100 900;
|
||||
font-style: oblique 0deg 10deg;
|
||||
src: url('static/Inter.var.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* variables */
|
||||
:root {
|
||||
--fg: white;
|
||||
--fg-faint: #a0a0a0;
|
||||
--fg-scrollbar: #404040;
|
||||
--bg: #101010;
|
||||
--bg-input: #202020;
|
||||
--bg-input-hover: #282828;
|
||||
--focus-color: #4060a0;
|
||||
|
||||
--gradient-clickable: linear-gradient(to bottom, #404040, var(--bg-input) 65%, var(--bg-input));
|
||||
--gradient-clickable-hover: linear-gradient(to bottom, #505050, var(--bg-input-hover) 65%, var(--bg-input-hover));
|
||||
|
||||
--timestamp-width: 70px;
|
||||
}
|
||||
|
||||
/* reset */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
color: var(--fg);
|
||||
font-family: Lodestone, 'Inter var', sans-serif;
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* layout and global styles */
|
||||
body {
|
||||
padding: 50px;
|
||||
height: 100dvh;
|
||||
background-color: var(--bg-input-hover);
|
||||
}
|
||||
|
||||
body > main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg);
|
||||
border-radius: 20px;
|
||||
|
||||
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* message list */
|
||||
section#messages {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.5;
|
||||
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
scrollbar-color: var(--fg-scrollbar) var(--bg);
|
||||
|
||||
&.more-messages::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 100px;
|
||||
width: calc(100% - 200px);
|
||||
height: 200px;
|
||||
background-image: radial-gradient(50% 20% at 50% 100%, #60a0ff40, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.timestamp {
|
||||
flex: 0 0 var(--timestamp-width);
|
||||
color: var(--fg-faint);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#timestamp-width-probe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* input bar, channel selector, ... */
|
||||
section#input {
|
||||
flex-grow: 0;
|
||||
padding: 20px;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
input, button, select {
|
||||
font-size: 1rem;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 20px;
|
||||
background-color: var(--bg-input);
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--focus-color);
|
||||
}
|
||||
}
|
||||
|
||||
button, select {
|
||||
padding: 5px 15px;
|
||||
border: 3px solid var(--bg-input);
|
||||
background-image: var(--gradient-clickable);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-input-hover);
|
||||
background-color: var(--bg-input-hover);
|
||||
background-image: var(--gradient-clickable-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.select-container, button {
|
||||
position: relative;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
flex-basis: 0;
|
||||
|
||||
&::before {
|
||||
/* "message-circle" icon from https://github.com/feathericons/feather, under MIT license */
|
||||
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>');
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
appearance: none;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding-left: calc(20px + 1.5rem);
|
||||
|
||||
&::before {
|
||||
/* "send" icon from https://github.com/feathericons/feather, under MIT license */
|
||||
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>');
|
||||
}
|
||||
}
|
||||
|
||||
.select-container::before, button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
background-color: var(--fg);
|
||||
mask-size: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.select-container::before {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
#chat-input {
|
||||
width: 100%;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
#channel-hint {
|
||||
position: absolute;
|
||||
top: -1.2em;
|
||||
left: 23px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 550;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*** mobile ***/
|
||||
@media ((max-width: 600px) and (orientation: portrait)) or (max-width: 400px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body > main {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
section#input {
|
||||
button {
|
||||
max-width: 0;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
button::before {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// websocket connection
|
||||
class WSConnection {
|
||||
constructor() {
|
||||
this.socket = new WebSocket('ws://192.168.2.106:9001/ws');
|
||||
this.socket.addEventListener('open', this.onWSOpen.bind(this));
|
||||
this.socket.addEventListener('close', this.onWSClose.bind(this));
|
||||
this.socket.addEventListener('message', this.onWSMessage.bind(this));
|
||||
}
|
||||
|
||||
onWSOpen() {
|
||||
// send request for initial data (channels, currently selected channel)
|
||||
}
|
||||
|
||||
onWSClose() {
|
||||
// open new websocket here? for mobile
|
||||
}
|
||||
|
||||
onWSMessage(event) {
|
||||
try {
|
||||
let eventData = JSON.parse(event.data);
|
||||
for (let message of eventData.messages)
|
||||
{
|
||||
addMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: error handling?
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
send(message) {
|
||||
this.socket.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
const ws = new WSConnection();
|
||||
|
||||
|
||||
// channel switcher
|
||||
function updateChannelHint(label) {
|
||||
document.getElementById('channel-hint').innerText = label;
|
||||
}
|
||||
document.getElementById('channel-select').addEventListener('change', (event) => {
|
||||
updateChannelHint(event.target.value);
|
||||
// TODO: send new channel to "backend"
|
||||
// ws.send(...);
|
||||
});
|
||||
|
||||
|
||||
// functions for handling the message list
|
||||
function scrollMessagesToBottom() {
|
||||
const messagesContainer = document.querySelector('#messages > .scroll-container');
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight - messagesContainer.offsetHeight;
|
||||
}
|
||||
|
||||
function messagesContainerIsScrolledToBottom() {
|
||||
const messagesContainer = document.querySelector('#messages > .scroll-container');
|
||||
return messagesContainer.scrollTop == messagesContainer.scrollHeight - messagesContainer.offsetHeight;
|
||||
}
|
||||
|
||||
function addMessage(messageData) {
|
||||
const scrolledToBottom = messagesContainerIsScrolledToBottom();
|
||||
|
||||
const liMessage = document.createElement('li');
|
||||
const spanTimestamp = document.createElement('span');
|
||||
spanTimestamp.classList.add('timestamp');
|
||||
const spanMessage = document.createElement('span');
|
||||
spanMessage.classList.add('message');
|
||||
|
||||
// suggestions, update with actual data model
|
||||
spanTimestamp.innerText = messageData.timestamp;
|
||||
spanMessage.innerHTML = messageData.messageHTML; // or build HTML in here
|
||||
|
||||
liMessage.appendChild(spanTimestamp);
|
||||
liMessage.appendChild(spanMessage);
|
||||
document.getElementById('messages-list').appendChild(liMessage);
|
||||
|
||||
if (scrolledToBottom) {
|
||||
scrollMessagesToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
document.getElementById('messages-list').innerHTML = '';
|
||||
}
|
||||
|
||||
//
|
||||
document.querySelector('#messages > .scroll-container').addEventListener('scroll', () => {
|
||||
const messagesContainer = document.querySelector('#messages > .scroll-container');
|
||||
if (!messagesContainerIsScrolledToBottom()) {
|
||||
messagesContainer.classList.add('more-messages');
|
||||
} else {
|
||||
messagesContainer.classList.remove('more-messages');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// handle message sending
|
||||
document.querySelector('#input > form').addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const message = chatInput.value;
|
||||
|
||||
fetch('/send', {
|
||||
method: 'POST',
|
||||
body: message,
|
||||
headers: {
|
||||
'Content-type': 'application/txt; charset=UTF-8'
|
||||
}
|
||||
});
|
||||
|
||||
chatInput.value = '';
|
||||
});
|
||||
|
||||
|
||||
// calculate timestamp width
|
||||
// to ensure that all timestamps have the same width. some typefaces have the same width across
|
||||
// all number glyphs, others do not. the solution below is very rudimentary; at the very least,
|
||||
// delaying it to account for font loading might make sense. perhaps there's an even better way?
|
||||
window.setTimeout(() => {
|
||||
const widthProbe = document.getElementById('timestamp-width-probe');
|
||||
widthProbe.innerText = '88:88'; // assume 8 to be widest glyph
|
||||
document.body.style.setProperty('--timestamp-width', Math.ceil(widthProbe.clientWidth) + 'px');
|
||||
}, 100);
|
||||
|
||||
// From kizer
|
||||
async function AddGfdStylesheet(gfdPath, texPath) {
|
||||
const texPromise = LoadTexAsBlob(texPath);
|
||||
const gfdPromise = LoadGfd(gfdPath);
|
||||
const texUrl = URL.createObjectURL(await texPromise);
|
||||
const gfd = await gfdPromise;
|
||||
|
||||
let stylesheets = [];
|
||||
for (const entry of gfd) {
|
||||
if (entry.width * entry.height <= 0)
|
||||
continue;
|
||||
|
||||
if (entry.redirect !== 0) {
|
||||
stylesheets[entry.redirect][0].push(entry.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
stylesheets[entry.id] = [
|
||||
[entry.id],
|
||||
[
|
||||
`background-position: -${entry.left}px -${entry.top}px`,
|
||||
`background-image: url('${texUrl}')`,
|
||||
`width: ${entry.width}px`,
|
||||
`height: ${entry.height}px`,
|
||||
`margin-bottom: -20px`
|
||||
].join(";"),
|
||||
[
|
||||
`background-position: -${entry.left * 2}px -${entry.top * 2 + 341}px`,
|
||||
`background-image: url('${texUrl}')`,
|
||||
`width: ${entry.width * 2}px`,
|
||||
`height: ${entry.height * 2}px`,
|
||||
`margin-bottom: -40px`
|
||||
].join(";")
|
||||
];
|
||||
}
|
||||
|
||||
let stylesheet = ".gfd-icon::before { content: ''; display: inline-block; overflow: hidden; vertical-align: top; height:0; }";
|
||||
for (const entry of stylesheets) {
|
||||
if (!entry)
|
||||
continue;
|
||||
|
||||
stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-${x}::before`).join(', ')}{${entry[1]};}`;
|
||||
stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-hq-${x}::before`).join(', ')}{${entry[2]};}`;
|
||||
}
|
||||
|
||||
const styleNode = document.createElement("style");
|
||||
styleNode.type = "text/css";
|
||||
styleNode.appendChild(document.createTextNode(stylesheet));
|
||||
document.head.appendChild(styleNode);
|
||||
}
|
||||
|
||||
async function LoadTexAsBlob(path) {
|
||||
const tex = ParseTex(await (await fetch(path)).arrayBuffer());
|
||||
if (tex.format !== 0x1450) // B8G8R8A8
|
||||
throw "Not supported";
|
||||
|
||||
const dataArray = new Uint8ClampedArray(tex.buffer, tex.offsetToSurface[0], tex.width * tex.height * 4);
|
||||
for (let i = 0; i < dataArray.length; i += 4) {
|
||||
const t = dataArray[i];
|
||||
dataArray[i] = dataArray[i + 2];
|
||||
dataArray[i + 2] = t;
|
||||
}
|
||||
const imageData = new ImageData(dataArray, tex.width, tex.height);
|
||||
const bitmap = await createImageBitmap(imageData);
|
||||
|
||||
const canvas = new OffscreenCanvas(tex.width, tex.height);
|
||||
canvas.getContext('bitmaprenderer').transferFromImageBitmap(bitmap);
|
||||
return await canvas.convertToBlob();
|
||||
}
|
||||
|
||||
async function LoadGfd(path) {
|
||||
const buffer = new DataView(await (await fetch(path)).arrayBuffer());
|
||||
const count = buffer.getInt32(8, true);
|
||||
const entries = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const offset = 0x10 + (i * 0x10);
|
||||
entries[i] = {
|
||||
id: buffer.getInt16(offset, true),
|
||||
left: buffer.getInt16(offset + 2, true),
|
||||
top: buffer.getInt16(offset + 4, true),
|
||||
width: buffer.getInt16(offset + 6, true),
|
||||
height: buffer.getInt16(offset + 8, true),
|
||||
unk0A: buffer.getInt16(offset + 10, true),
|
||||
redirect: buffer.getInt16(offset + 12, true),
|
||||
unk0E: buffer.getInt16(offset + 14, true),
|
||||
};
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function ParseTex(arrayBuffer) {
|
||||
const buffer = new DataView(arrayBuffer);
|
||||
const type = buffer.getInt32(0, true);
|
||||
const format = buffer.getInt32(4, true);
|
||||
const width = buffer.getInt16(8, true);
|
||||
const height = buffer.getInt16(10, true);
|
||||
const depth = buffer.getInt16(12, true);
|
||||
const mipsAndFlag = buffer.getInt8(14, true);
|
||||
const arraySize = buffer.getInt8(15, true);
|
||||
const lodOffsets = [buffer.getInt32(16, true), buffer.getInt32(20, true), buffer.getInt32(24, true)];
|
||||
const offsetToSurface = [buffer.getInt32(28, true), buffer.getInt32(32, true), buffer.getInt32(36, true), buffer.getInt32(40, true), buffer.getInt32(44, true), buffer.getInt32(48, true), buffer.getInt32(52, true), buffer.getInt32(56, true), buffer.getInt32(60, true), buffer.getInt32(64, true), buffer.getInt32(68, true), buffer.getInt32(72, true), buffer.getInt32(76, true)];
|
||||
return {
|
||||
buffer: arrayBuffer,
|
||||
type,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
mipsAndFlag,
|
||||
arraySize,
|
||||
lodOffsets,
|
||||
offsetToSurface,
|
||||
};
|
||||
}
|
||||
|
||||
AddGfdStylesheet("/files/gfdata.gfd", "/files/fonticon_ps5.tex");
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Authentication</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/start.css">
|
||||
</head>
|
||||
<body>
|
||||
<div style="text-align: center;">
|
||||
<h3 style="color: #fff">Authcode</h3>
|
||||
<form action="/auth" method="POST">
|
||||
<input style="color: #fff; background: #121212" type="password" id="authcode" name="authcode">
|
||||
<input style="color: #fff; background: #121212" type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
<img src="http://192.168.2.106:9000/emote/Sure">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -255,13 +255,20 @@ internal class MessageManager : IAsyncDisposable
|
||||
if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle())
|
||||
Store.UpsertMessage(message);
|
||||
|
||||
var currentTabId = Plugin.ChatLogWindow.CurrentTab?.Identifier ?? Guid.Empty;
|
||||
var currentMatches = Plugin.ChatLogWindow.CurrentTab?.Matches(message) ?? false;
|
||||
foreach (var tab in Plugin.Config.Tabs)
|
||||
{
|
||||
var unread = !(tab.UnreadMode == UnreadMode.Unseen && Plugin.ChatLogWindow.CurrentTab != tab && currentMatches);
|
||||
|
||||
if (tab.Matches(message))
|
||||
{
|
||||
tab.AddMessage(message, unread);
|
||||
|
||||
if (tab.Identifier == currentTabId)
|
||||
Plugin.ServerCore.SendNewMessage(message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using ChatTwo.GameFunctions;
|
||||
using ChatTwo.Http;
|
||||
using ChatTwo.Ipc;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Ui;
|
||||
@@ -59,6 +60,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
internal ExtraChat ExtraChat { get; }
|
||||
internal FontManager FontManager { get; }
|
||||
|
||||
internal ServerCore ServerCore { get; }
|
||||
|
||||
internal int DeferredSaveFrames = -1;
|
||||
|
||||
internal DateTime GameStarted { get; }
|
||||
@@ -126,9 +129,13 @@ public sealed class Plugin : IDalamudPlugin
|
||||
// profiling difficult.
|
||||
AutoTranslate.PreloadCache();
|
||||
#endif
|
||||
|
||||
ServerCore = new ServerCore(this);
|
||||
Task.Run(() => ServerCore.Start());
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Plugin load threw an error, turning off plugin");
|
||||
Dispose();
|
||||
// Re-throw the exception to fail the plugin load.
|
||||
throw;
|
||||
@@ -160,6 +167,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
Commands?.Dispose();
|
||||
|
||||
EmoteCache.Dispose();
|
||||
ServerCore.Dispose();
|
||||
}
|
||||
|
||||
private void Draw()
|
||||
|
||||
Generated
+63
@@ -3407,6 +3407,33 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface.
|
||||
/// </summary>
|
||||
internal static string Options_Webinterface_Tab {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Webinterface_Tab", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Enables the webinterface that can be accessed with a browser..
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceEnable_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceEnable_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Enable.
|
||||
/// </summary>
|
||||
internal static string Options_WebinterfaceEnable_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_WebinterfaceEnable_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Window opacity.
|
||||
/// </summary>
|
||||
@@ -3541,5 +3568,41 @@ namespace ChatTwo.Resources {
|
||||
return ResourceManager.GetString("UnreadMode_Unseen_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your current password to access the webinterface:.
|
||||
/// </summary>
|
||||
internal static string Webinterface_CurrentPassword {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_CurrentPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Reset your password and invalidate all session tokens..
|
||||
/// </summary>
|
||||
internal static string Webinterface_PasswordReset_Tooltip {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_PasswordReset_Tooltip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Webinterface Status:.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Status {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Status", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Active:.
|
||||
/// </summary>
|
||||
internal static string Webinterface_Status_Active {
|
||||
get {
|
||||
return ResourceManager.GetString("Webinterface_Status_Active", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +463,9 @@
|
||||
<data name="Options_Miscellaneous_Tab">
|
||||
<value>Miscellaneous</value>
|
||||
</data>
|
||||
<data name="Options_Webinterface_Tab">
|
||||
<value>Webinterface</value>
|
||||
</data>
|
||||
<data name="LanguageOverride_None">
|
||||
<value>Use Dalamud's default language</value>
|
||||
</data>
|
||||
@@ -547,6 +550,12 @@
|
||||
<data name="Options_SortAutoTranslate_Description">
|
||||
<value>If this is enabled, the Auto Translate list will be sorted alphabetically.</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Name">
|
||||
<value>Enable</value>
|
||||
</data>
|
||||
<data name="Options_WebinterfaceEnable_Description">
|
||||
<value>Enables the webinterface that can be accessed with a browser.</value>
|
||||
</data>
|
||||
<data name="Options_OverrideStyle_Name">
|
||||
<value>Override Style</value>
|
||||
</data>
|
||||
@@ -559,6 +568,18 @@
|
||||
<data name="Options_ChatTabBackwardKeybind_Name">
|
||||
<value>Cycle chat tab backwards keybind</value>
|
||||
</data>
|
||||
<data name="Webinterface_CurrentPassword">
|
||||
<value>Your current password to access the webinterface:</value>
|
||||
</data>
|
||||
<data name="Webinterface_PasswordReset_Tooltip">
|
||||
<value>Reset your password and invalidate all session tokens.</value>
|
||||
</data>
|
||||
<data name="Webinterface_Status">
|
||||
<value>Webinterface Status:</value>
|
||||
</data>
|
||||
<data name="Webinterface_Status_Active">
|
||||
<value>Active:</value>
|
||||
</data>
|
||||
<data name="Keybind_None">
|
||||
<value>none set</value>
|
||||
</data>
|
||||
|
||||
+103
-81
@@ -58,7 +58,7 @@ public sealed class ChatLogWindow : Window
|
||||
private int InputBacklogIdx = -1;
|
||||
private int LastTab { get; set; }
|
||||
private InputChannel? TempChannel;
|
||||
private TellTarget? TellTarget;
|
||||
internal TellTarget? TellTarget;
|
||||
public bool TellSpecial;
|
||||
private readonly Stopwatch LastResize = new();
|
||||
private AutoCompleteInfo? AutoCompleteInfo;
|
||||
@@ -555,84 +555,7 @@ public sealed class ChatLogWindow : Window
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
|
||||
{
|
||||
if (TellTarget != null)
|
||||
{
|
||||
var playerName = TellTarget.Name;
|
||||
if (ScreenshotMode)
|
||||
// Note: don't use HidePlayerInString here because
|
||||
// abbreviation settings do not affect this.
|
||||
playerName = HashPlayer(TellTarget.Name, TellTarget.World);
|
||||
var world = WorldSheet.GetRow(TellTarget.World)?.Name?.RawString ?? "???";
|
||||
|
||||
DrawChunks(new Chunk[]
|
||||
{
|
||||
new TextChunk(ChunkSource.None, null, "Tell "),
|
||||
new TextChunk(ChunkSource.None, null, playerName),
|
||||
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
|
||||
new TextChunk(ChunkSource.None, null, world),
|
||||
});
|
||||
}
|
||||
else if (TempChannel != null)
|
||||
{
|
||||
if (TempChannel.Value.IsLinkshell())
|
||||
{
|
||||
var idx = (uint) TempChannel.Value - (uint) InputChannel.Linkshell1;
|
||||
var lsName = Plugin.Functions.Chat.GetLinkshellName(idx);
|
||||
ImGui.TextUnformatted($"LS #{idx + 1}: {lsName}");
|
||||
}
|
||||
else if (TempChannel.Value.IsCrossLinkshell())
|
||||
{
|
||||
var idx = (uint) TempChannel.Value - (uint) InputChannel.CrossLinkshell1;
|
||||
var cwlsName = Plugin.Functions.Chat.GetCrossLinkshellName(idx);
|
||||
ImGui.TextUnformatted($"CWLS [{idx + 1}]: {cwlsName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(TempChannel.Value.ToChatType().Name());
|
||||
}
|
||||
}
|
||||
else if (activeTab is { Channel: { } channel })
|
||||
{
|
||||
// We cannot lookup ExtraChat channel names from index over
|
||||
// IPC so we just don't show the name if it's the tabs
|
||||
// channel.
|
||||
//
|
||||
// We don't call channel.ToChatType().Name() as it has the
|
||||
// long name as used in the settings window.
|
||||
ImGui.TextUnformatted(channel.IsExtraChatLinkshell() ? $"ECLS [{channel.LinkshellIndex() + 1}]" : channel.ToChatType().Name());
|
||||
}
|
||||
else if (Plugin.ExtraChat.ChannelOverride is var (overrideName, _))
|
||||
{
|
||||
ImGui.TextUnformatted(overrideName);
|
||||
}
|
||||
else if (ScreenshotMode && Plugin.Functions.Chat.Channel is (InputChannel.Tell, _, var tellPlayerName, var tellWorldId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tellPlayerName) && tellWorldId != 0)
|
||||
{
|
||||
// Note: don't use HidePlayerInString here because
|
||||
// abbreviation settings do not affect this.
|
||||
var playerName = HashPlayer(tellPlayerName, tellWorldId);
|
||||
var world = WorldSheet.GetRow(tellWorldId)?.Name?.RawString ?? "???";
|
||||
|
||||
DrawChunks(new Chunk[]
|
||||
{
|
||||
new TextChunk(ChunkSource.None, null, "Tell "),
|
||||
new TextChunk(ChunkSource.None, null, playerName),
|
||||
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
|
||||
new TextChunk(ChunkSource.None, null, world),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We still need to censor the name if we couldn't read
|
||||
// valid data.
|
||||
ImGui.TextUnformatted("Tell");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawChunks(Plugin.Functions.Chat.Channel.Name);
|
||||
}
|
||||
DrawChannelName(activeTab);
|
||||
}
|
||||
|
||||
var beforeIcon = ImGui.GetCursorPos();
|
||||
@@ -826,6 +749,105 @@ public sealed class ChatLogWindow : Window
|
||||
GameFunctions.GameFunctions.ClickNoviceNetworkButton();
|
||||
}
|
||||
|
||||
private void DrawChannelName(Tab? activeTab)
|
||||
{
|
||||
DrawChunks(ReadChannelName(activeTab));
|
||||
}
|
||||
|
||||
internal Chunk[] ReadChannelName(Tab? activeTab)
|
||||
{
|
||||
Chunk[] channelNameChunks;
|
||||
if (TellTarget != null)
|
||||
{
|
||||
var playerName = TellTarget.Name;
|
||||
if (ScreenshotMode)
|
||||
// Note: don't use HidePlayerInString here because
|
||||
// abbreviation settings do not affect this.
|
||||
playerName = HashPlayer(TellTarget.Name, TellTarget.World);
|
||||
var world = WorldSheet.GetRow(TellTarget.World)?.Name?.RawString ?? "???";
|
||||
|
||||
channelNameChunks = new Chunk[]
|
||||
{
|
||||
new TextChunk(ChunkSource.None, null, "Tell "),
|
||||
new TextChunk(ChunkSource.None, null, playerName),
|
||||
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
|
||||
new TextChunk(ChunkSource.None, null, world),
|
||||
};
|
||||
}
|
||||
else if (TempChannel != null)
|
||||
{
|
||||
string name;
|
||||
if (TempChannel.Value.IsLinkshell())
|
||||
{
|
||||
var idx = (uint) TempChannel.Value - (uint) InputChannel.Linkshell1;
|
||||
var lsName = Plugin.Functions.Chat.GetLinkshellName(idx);
|
||||
name = $"LS #{idx + 1}: {lsName}";
|
||||
}
|
||||
else if (TempChannel.Value.IsCrossLinkshell())
|
||||
{
|
||||
var idx = (uint) TempChannel.Value - (uint) InputChannel.CrossLinkshell1;
|
||||
var cwlsName = Plugin.Functions.Chat.GetCrossLinkshellName(idx);
|
||||
name = $"CWLS [{idx + 1}]: {cwlsName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
name = TempChannel.Value.ToChatType().Name();
|
||||
}
|
||||
|
||||
channelNameChunks = [new TextChunk(ChunkSource.None, null, name)];
|
||||
}
|
||||
else if (activeTab is { Channel: { } channel })
|
||||
{
|
||||
// We cannot lookup ExtraChat channel names from index over
|
||||
// IPC so we just don't show the name if it's the tabs
|
||||
// channel.
|
||||
//
|
||||
// We don't call channel.ToChatType().Name() as it has the
|
||||
// long name as used in the settings window.
|
||||
channelNameChunks = new Chunk[]
|
||||
{
|
||||
new TextChunk(ChunkSource.None, null, channel.IsExtraChatLinkshell() ? $"ECLS [{channel.LinkshellIndex() + 1}]" : channel.ToChatType().Name())
|
||||
};
|
||||
}
|
||||
else if (Plugin.ExtraChat.ChannelOverride is var (overrideName, _))
|
||||
{
|
||||
channelNameChunks = new Chunk[]
|
||||
{
|
||||
new TextChunk(ChunkSource.None, null, overrideName)
|
||||
};
|
||||
}
|
||||
else if (ScreenshotMode && Plugin.Functions.Chat.Channel is (InputChannel.Tell, _, var tellPlayerName, var tellWorldId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tellPlayerName) && tellWorldId != 0)
|
||||
{
|
||||
// Note: don't use HidePlayerInString here because
|
||||
// abbreviation settings do not affect this.
|
||||
var playerName = HashPlayer(tellPlayerName, tellWorldId);
|
||||
var world = WorldSheet.GetRow(tellWorldId)?.Name?.RawString ?? "???";
|
||||
|
||||
channelNameChunks = new Chunk[]
|
||||
{
|
||||
new TextChunk(ChunkSource.None, null, "Tell "),
|
||||
new TextChunk(ChunkSource.None, null, playerName),
|
||||
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
|
||||
new TextChunk(ChunkSource.None, null, world),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// We still need to censor the name if we couldn't read
|
||||
// valid data.
|
||||
channelNameChunks = [new TextChunk(ChunkSource.None, null, "Tell")];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
channelNameChunks = Plugin.Functions.Chat.Channel.Name.ToArray();
|
||||
}
|
||||
|
||||
return channelNameChunks;
|
||||
}
|
||||
|
||||
internal void SetChannel(InputChannel? channel)
|
||||
{
|
||||
channel ??= InputChannel.Say;
|
||||
@@ -852,7 +874,7 @@ public sealed class ChatLogWindow : Window
|
||||
GameFunctions.Chat.SetChannel(channel.Value);
|
||||
}
|
||||
|
||||
private void SendChatBox(Tab? activeTab)
|
||||
internal void SendChatBox(Tab? activeTab)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Chat))
|
||||
{
|
||||
@@ -1727,7 +1749,7 @@ public sealed class ChatLogWindow : Window
|
||||
|
||||
}
|
||||
|
||||
private string HidePlayerInString(string str, string playerName, uint worldId)
|
||||
internal string HidePlayerInString(string str, string playerName, uint worldId)
|
||||
{
|
||||
var expected = Plugin.Functions.Chat.AbbreviatePlayerName(playerName);
|
||||
var hash = HashPlayer(playerName, worldId);
|
||||
|
||||
@@ -37,10 +37,11 @@ public sealed class SettingsWindow : Window
|
||||
new ChatLog(Plugin, Mutable),
|
||||
new Emote(Plugin, Mutable),
|
||||
new Preview(Plugin, Mutable),
|
||||
new Ui.SettingsTabs.Fonts(Mutable),
|
||||
new Fonts(Mutable),
|
||||
new ChatColours(Plugin, Mutable),
|
||||
new Tabs(Plugin, Mutable),
|
||||
new Database(Plugin, Mutable),
|
||||
new Webinterface(Plugin, Mutable),
|
||||
new Miscellaneous(Mutable),
|
||||
new Changelog(Mutable),
|
||||
new About(),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class Webinterface(Plugin plugin, Configuration mutable) : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; } = plugin;
|
||||
private Configuration Mutable { get; } = mutable;
|
||||
public string Name => Language.Options_Webinterface_Tab + "###tabs-Webinterface";
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudWhite, "On checking 'Enabled' this will enable and load up Chat2's built-in web interface, which will allow devices on your network to access in-game chat. This feature may be used to allow a phone or another computer to see Chat2 activity, switch channels, and send messages as though you were typing in FFXIV itself.");
|
||||
ImGui.Spacing();
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.HealerGreen, "Note: This will require at least a semi-modern browser in order to function correctly.");
|
||||
ImGui.Spacing();
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudOrange, "For reasons of account security, this feature is not intended for use outside of your local network, you have been warned!");
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Spacing();
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudViolet, "Do Not:");
|
||||
using (ImRaii.PushIndent(15.0f))
|
||||
{
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudViolet, "- Forward the ports used (9000 and 9001)");
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudViolet, "- Share your authentication code with anyone else");
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudViolet, "- Expect multi-boxing to work with this (only first client is tracked and utilised)");
|
||||
}
|
||||
ImGui.Spacing();
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudOrange, "No support will be provided if any of the 'Do Not' clauses aren't respected and adhered to appropriately.");
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Checkbox(Language.Options_WebinterfaceEnable_Name, ref Mutable.WebinterfaceEnabled);
|
||||
ImGuiUtil.HelpText(Language.Options_WebinterfaceEnable_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (!Mutable.WebinterfaceEnabled)
|
||||
return;
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.DalamudOrange, Language.Webinterface_CurrentPassword);
|
||||
ImGui.TextUnformatted(Mutable.WebinterfacePassword);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Recycle, tooltip: Language.Webinterface_PasswordReset_Tooltip))
|
||||
Mutable.WebinterfacePassword = WebinterfaceUtil.GenerateSimpleAuthCode();
|
||||
|
||||
ImGuiUtil.WrappedTextWithColor(ImGuiColors.HealerGreen, Language.Webinterface_Status);
|
||||
using (ImRaii.PushIndent(10.0f))
|
||||
{
|
||||
ImGui.TextUnformatted(Language.Webinterface_Status_Active);
|
||||
ImGui.SameLine();
|
||||
|
||||
var isActive = Plugin.ServerCore.GetStats();
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, isActive ? ImGuiColors.HealerGreen : ImGuiColors.DalamudOrange))
|
||||
{
|
||||
ImGui.TextUnformatted($"{(isActive ? FontAwesomeIcon.Check.ToIconString() : FontAwesomeIcon.Cross.ToIconString())}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using System.Numerics;
|
||||
namespace ChatTwo.Util;
|
||||
|
||||
internal static class ColourUtil {
|
||||
private static (byte r, byte g, byte b, byte a) RgbaToComponents(uint rgba) {
|
||||
internal static (byte r, byte g, byte b, byte a) RgbaToComponents(uint rgba) {
|
||||
var r = (byte) ((rgba & 0xFF000000) >> 24);
|
||||
var g = (byte) ((rgba & 0xFF0000) >> 16);
|
||||
var b = (byte) ((rgba & 0xFF00) >> 8);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace ChatTwo.Util;
|
||||
|
||||
@@ -145,4 +147,11 @@ internal static class IconUtil {
|
||||
return new GfdFileView(new ReadOnlySpan<byte>(Unsafe.AsPointer(ref GfdFile[0]), GfdFile.Length));
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] ImageToRaw(this Image<Rgba32> image)
|
||||
{
|
||||
var data = new byte[4 * image.Width * image.Height];
|
||||
image.CopyPixelDataTo(data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,6 +668,14 @@ internal static class ImGuiUtil
|
||||
}
|
||||
}
|
||||
|
||||
public static void WrappedTextWithColor(Vector4 color, string text)
|
||||
{
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
||||
{
|
||||
ImGui.TextWrapped(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Used to avoid pops if condition is false for Push.
|
||||
private static void Nop() { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ChatTwo.Util;
|
||||
|
||||
public class WebinterfaceUtil
|
||||
{
|
||||
private static readonly Random Rng = new();
|
||||
|
||||
public static string GenerateSimpleAuthCode()
|
||||
{
|
||||
return (100000 + Rng.Next() % 100000).ToString()[1..];
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,26 @@
|
||||
"resolved": "2.1.13",
|
||||
"contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ=="
|
||||
},
|
||||
"EmbedIO": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.5.2, )",
|
||||
"resolved": "3.5.2",
|
||||
"contentHash": "YU4j+3XvuO8/VPkNf7KWOF1TpMhnyVhXnPsG1mvnDhTJ9D5BZOFXVDvCpE/SkQ1AJ0Aa+dXOVSW3ntgmLL7aJg==",
|
||||
"dependencies": {
|
||||
"Unosquare.Swan.Lite": "3.1.0"
|
||||
}
|
||||
},
|
||||
"HtmlSanitizer": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.1.870, )",
|
||||
"resolved": "8.1.870",
|
||||
"contentHash": "bQWYaKg8PrlgnhM9sPALl0UorpjWQkPTQiSTVyvm8imqF9lCLqBmtC0adUDi8xUYcdg6SJC+aHCw1MOjcg+Wnw==",
|
||||
"dependencies": {
|
||||
"AngleSharp": "[0.17.1]",
|
||||
"AngleSharp.Css": "[0.17.0]",
|
||||
"System.Collections.Immutable": "8.0.0"
|
||||
}
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.5.140, )",
|
||||
@@ -41,6 +61,43 @@
|
||||
"resolved": "3.1.5",
|
||||
"contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g=="
|
||||
},
|
||||
"Watson.Lite": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.2.2, )",
|
||||
"resolved": "6.2.2",
|
||||
"contentHash": "yRYcaRFSnqwy/rrPd+WIFLyvNfgTANo447KiK7j1mKpgUIYPNDAKYxqj9wkDVfvGIHxzt4z3CFK6eWOGWUYsuQ==",
|
||||
"dependencies": {
|
||||
"CavemanTcp": "2.0.2",
|
||||
"Watson.Core": "6.2.2"
|
||||
}
|
||||
},
|
||||
"AngleSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.17.1",
|
||||
"contentHash": "5MPI4bbixlwxb0W/smOMeIR+QlxMy5/5jD+WnIAw4pBC+7AhLPe5bS3cLgQMJyvd6q0A48sG+uYOt/ep406GLA==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Text.Encoding.CodePages": "6.0.0"
|
||||
}
|
||||
},
|
||||
"AngleSharp.Css": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.17.0",
|
||||
"contentHash": "bg0AcugmX6BFEi/DHG61QrwRU8iuiX4H8LZehdIzYdqOM/dgb3BsCTzNIcc1XADn4+xfQEdVwJYTSwUxroL4vg==",
|
||||
"dependencies": {
|
||||
"AngleSharp": "[0.17.0, 0.18.0)"
|
||||
}
|
||||
},
|
||||
"CavemanTcp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.2",
|
||||
"contentHash": "T161WxkgNTMC5SQPPDfLCGCD2j3YTvF0ZUGy/1pcGaYi6y7mcSzPymgVzG/6mOjAxJNGotK8hf1iOD8eT3bbhQ=="
|
||||
},
|
||||
"IpMatcher": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.4.4",
|
||||
"contentHash": "93zP6KaDdRttUI3fFet6mYQGed1gTVK2amIOxpq4tDl3NRY+2tP/iA30hvRrs+ZY8u8KGF/J7kX6l7jrhKHfZA=="
|
||||
},
|
||||
"MessagePack.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.5.140",
|
||||
@@ -59,6 +116,11 @@
|
||||
"resolved": "17.6.3",
|
||||
"contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA=="
|
||||
},
|
||||
"RegexMatcher": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.8",
|
||||
"contentHash": "h3wa6sKJnyojCH2wJpvHdQRGWn+eKiSYGKPInIzvdl+SaKhUN9Qpr0CK4A3aqrYFBTEJKFN7pzYlNR4S5gZnaw=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.6",
|
||||
@@ -89,6 +151,16 @@
|
||||
"SQLitePCLRaw.core": "2.1.6"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.1",
|
||||
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.3",
|
||||
@@ -98,6 +170,62 @@
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Encodings.Web": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.4",
|
||||
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
|
||||
"dependencies": {
|
||||
"System.Text.Encodings.Web": "8.0.0"
|
||||
}
|
||||
},
|
||||
"System.ValueTuple": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
|
||||
},
|
||||
"Timestamps": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.9",
|
||||
"contentHash": "xdCVp+T4VFXOT+Ube2Cz527fnFXFUxQK24uPMGcCmICIpIcGCXPtI7vKLnWsJ6Nfc1B92tNBK9e/I8NDq2ee6g=="
|
||||
},
|
||||
"Unosquare.Swan.Lite": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.0",
|
||||
"contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==",
|
||||
"dependencies": {
|
||||
"System.ValueTuple": "4.5.0"
|
||||
}
|
||||
},
|
||||
"UrlMatcher": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "IavFDKxhcg5ehi3baGEejxnZoj4bqfkDnoMxTV+6Y4WHVxW8wgxTzdSl9q5WSMi366LG0amDfOxaUJxuHawNLw=="
|
||||
},
|
||||
"Watson.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.2.2",
|
||||
"contentHash": "lqdT+foblghBKLXuvXBFLJmO9J0TkPCBiQFH4xg4txFM7osm1jvF2IzYjor6w1NL+kNDhvlCRkBNaPSSxfJO/A==",
|
||||
"dependencies": {
|
||||
"IpMatcher": "1.0.4.4",
|
||||
"RegexMatcher": "1.0.8",
|
||||
"System.Text.Json": "8.0.4",
|
||||
"Timestamps": "1.0.9",
|
||||
"UrlMatcher": "3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user