BTTV emote support part 1 & keep Input focus option
This commit is contained in:
@@ -55,6 +55,7 @@
|
|||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.4" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.4" />
|
||||||
<PackageReference Include="Pidgin" Version="3.2.2"/>
|
<PackageReference Include="Pidgin" Version="3.2.2"/>
|
||||||
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0"/>
|
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0"/>
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||||
<PackageReference Include="XivCommon" Version="9.0.0"/>
|
<PackageReference Include="XivCommon" Version="9.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ internal class Configuration : IPluginConfiguration
|
|||||||
public bool SortAutoTranslate;
|
public bool SortAutoTranslate;
|
||||||
public bool CollapseDuplicateMessages;
|
public bool CollapseDuplicateMessages;
|
||||||
public bool PlaySounds = true;
|
public bool PlaySounds = true;
|
||||||
|
public bool KeepInputFocus = true;
|
||||||
public int MaxLinesToRender = 10_000;
|
public int MaxLinesToRender = 10_000;
|
||||||
|
|
||||||
public bool FontsEnabled = true;
|
public bool FontsEnabled = true;
|
||||||
@@ -85,6 +86,7 @@ internal class Configuration : IPluginConfiguration
|
|||||||
SortAutoTranslate = other.SortAutoTranslate;
|
SortAutoTranslate = other.SortAutoTranslate;
|
||||||
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
||||||
PlaySounds = other.PlaySounds;
|
PlaySounds = other.PlaySounds;
|
||||||
|
KeepInputFocus = other.KeepInputFocus;
|
||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
FontsEnabled = other.FontsEnabled;
|
FontsEnabled = other.FontsEnabled;
|
||||||
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Dalamud.Interface.Internal;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using ImGuiNET;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
|
namespace ChatTwo;
|
||||||
|
|
||||||
|
public static class EmoteCache
|
||||||
|
{
|
||||||
|
private struct Top100
|
||||||
|
{
|
||||||
|
[JsonPropertyName("emote")]
|
||||||
|
public Emote Emote { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Emote
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public string Code { get; set; }
|
||||||
|
[JsonPropertyName("imageType")]
|
||||||
|
public string ImageType { get; set; }
|
||||||
|
[JsonPropertyName("animated")]
|
||||||
|
public bool Animated { get; set; }
|
||||||
|
};
|
||||||
|
|
||||||
|
private const string BetterTTV = "https://api.betterttv.net/3";
|
||||||
|
private const string GlobalEmotes = $"{BetterTTV}/cached/emotes/global";
|
||||||
|
private const string Top100Emotes = $"{BetterTTV}/emotes/shared/top?limit=100";
|
||||||
|
private const string EmotePath = "https://cdn.betterttv.net/emote/{0}/3x";
|
||||||
|
|
||||||
|
private static readonly bool IsBTTVDataLoaded;
|
||||||
|
private static readonly Dictionary<string, IEmote> EmoteImages = new();
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, Emote> Cache = new();
|
||||||
|
|
||||||
|
private static readonly string[] EmoteCodeArray = [];
|
||||||
|
|
||||||
|
static EmoteCache()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var globalCache = new HttpClient().GetAsync(GlobalEmotes)
|
||||||
|
.Result
|
||||||
|
.Content
|
||||||
|
.ReadAsStringAsync()
|
||||||
|
.Result;
|
||||||
|
|
||||||
|
foreach (var emote in JsonSerializer.Deserialize<Emote[]>(globalCache)!)
|
||||||
|
Cache.TryAdd(emote.Code, emote);
|
||||||
|
|
||||||
|
var top100 = new HttpClient().GetAsync(Top100Emotes)
|
||||||
|
.Result
|
||||||
|
.Content
|
||||||
|
.ReadAsStringAsync()
|
||||||
|
.Result;
|
||||||
|
|
||||||
|
Plugin.Log.Information(top100);
|
||||||
|
var json = JsonSerializer.Deserialize<List<Top100>>(top100);
|
||||||
|
foreach (var emote in json)
|
||||||
|
{
|
||||||
|
Plugin.Log.Information($"emote {emote.Emote.Code}");
|
||||||
|
Cache.TryAdd(emote.Emote.Code, emote.Emote);
|
||||||
|
}
|
||||||
|
|
||||||
|
EmoteCodeArray = Cache.Keys.ToArray();
|
||||||
|
IsBTTVDataLoaded = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool Exists(string code)
|
||||||
|
{
|
||||||
|
return IsBTTVDataLoaded && EmoteCodeArray.Contains(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IEmote? GetEmote(string code)
|
||||||
|
{
|
||||||
|
if (!IsBTTVDataLoaded)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!Cache.TryGetValue(code, out var emoteDetail))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (EmoteImages.TryGetValue(emoteDetail.Id, out var emote))
|
||||||
|
return emote;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (emoteDetail.ImageType == "gif")
|
||||||
|
{
|
||||||
|
var animatedEmote = new ImGuiGif().Prepare(emoteDetail);
|
||||||
|
EmoteImages.Add(emoteDetail.Id, animatedEmote);
|
||||||
|
return animatedEmote;
|
||||||
|
}
|
||||||
|
|
||||||
|
var staticEmote = new ImGuiEmote().Prepare(emoteDetail);
|
||||||
|
EmoteImages.Add(emoteDetail.Id, staticEmote);
|
||||||
|
|
||||||
|
return staticEmote;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Plugin.Log.Error("Failed to convert");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IEmote
|
||||||
|
{
|
||||||
|
public bool IsLoaded = false;
|
||||||
|
|
||||||
|
public bool IsAnimated = false;
|
||||||
|
public IDalamudTextureWrap Texture;
|
||||||
|
|
||||||
|
public virtual void Draw(Vector2 size)
|
||||||
|
{
|
||||||
|
ImGui.Image(Texture.ImGuiHandle, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<byte[]> LoadAsync(Emote emote)
|
||||||
|
{
|
||||||
|
var dir = Path.Join(Plugin.Interface.ConfigDirectory.FullName, "emotes");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
byte[] image;
|
||||||
|
var filePath = Path.Join(dir, $"{emote.Id}.{emote.ImageType}");
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
image = await File.ReadAllBytesAsync(filePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var content = await new HttpClient().GetAsync(EmotePath.Format(emote.Id));
|
||||||
|
image = await content.Content.ReadAsByteArrayAsync();
|
||||||
|
|
||||||
|
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||||
|
stream.Write(image, 0, image.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ImGuiEmote : IEmote
|
||||||
|
{
|
||||||
|
public ImGuiEmote Prepare(Emote emote)
|
||||||
|
{
|
||||||
|
Task.Run(() => Load(emote));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Load(Emote emote)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var image = await LoadAsync(emote);
|
||||||
|
if (image.Length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Texture = await Plugin.Interface.UiBuilder.LoadImageAsync(image);
|
||||||
|
IsLoaded = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ImGuiGif : IEmote
|
||||||
|
{
|
||||||
|
private List<(IDalamudTextureWrap Texture, float Delay)> Frames = [];
|
||||||
|
private float FrameTimer;
|
||||||
|
private int CurrentFrame;
|
||||||
|
private ulong GlobalFrameCount;
|
||||||
|
|
||||||
|
public bool IsPaused;
|
||||||
|
|
||||||
|
public override void Draw(Vector2 size)
|
||||||
|
{
|
||||||
|
if (Frames.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (CurrentFrame >= Frames.Count)
|
||||||
|
{
|
||||||
|
CurrentFrame = 0;
|
||||||
|
FrameTimer = -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame = Frames[CurrentFrame];
|
||||||
|
if (FrameTimer <= 0.0f)
|
||||||
|
FrameTimer = frame.Delay;
|
||||||
|
|
||||||
|
ImGui.Image(frame.Texture.ImGuiHandle, size);
|
||||||
|
|
||||||
|
if (IsPaused)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (GlobalFrameCount != Plugin.Interface.UiBuilder.FrameCount)
|
||||||
|
{
|
||||||
|
GlobalFrameCount = Plugin.Interface.UiBuilder.FrameCount;
|
||||||
|
|
||||||
|
FrameTimer -= ImGui.GetIO().DeltaTime;
|
||||||
|
if (FrameTimer <= 0f)
|
||||||
|
CurrentFrame++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Frames.ForEach(f => f.Texture.Dispose());
|
||||||
|
Frames.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImGuiGif Prepare(Emote emote)
|
||||||
|
{
|
||||||
|
Task.Run(() => Load(emote));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Load(Emote emote)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var image = await LoadAsync(emote);
|
||||||
|
if (image.Length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var ms = new MemoryStream(image);
|
||||||
|
using var img = Image.Load<Rgba32>(ms);
|
||||||
|
if (img.Frames.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var frames = new List<(IDalamudTextureWrap Tex, float Delay)>();
|
||||||
|
foreach (var frame in img.Frames)
|
||||||
|
{
|
||||||
|
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
||||||
|
var buffer = new byte[4 * frame.Width * frame.Height];
|
||||||
|
frame.CopyPixelDataTo(buffer);
|
||||||
|
var tex = await Plugin.Interface.UiBuilder.LoadImageRawAsync(buffer, frame.Width, frame.Height, 4);
|
||||||
|
frames.Add((tex, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
Frames = frames;
|
||||||
|
IsLoaded = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
-43
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text;
|
||||||
using ChatTwo.Code;
|
using ChatTwo.Code;
|
||||||
using ChatTwo.Util;
|
using ChatTwo.Util;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
@@ -77,7 +78,7 @@ internal class Message
|
|||||||
Date = DateTimeOffset.UtcNow;
|
Date = DateTimeOffset.UtcNow;
|
||||||
Code = code;
|
Code = code;
|
||||||
Sender = sender;
|
Sender = sender;
|
||||||
Content = ReplaceContentURLs(content);
|
Content = CheckMessageContent(content);
|
||||||
SenderSource = senderSource;
|
SenderSource = senderSource;
|
||||||
ContentSource = contentSource;
|
ContentSource = contentSource;
|
||||||
SortCode = new SortCode(Code.Type, Code.Source);
|
SortCode = new SortCode(Code.Type, Code.Source);
|
||||||
@@ -130,28 +131,7 @@ internal class Message
|
|||||||
return Guid.Empty;
|
return Guid.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private List<Chunk> CheckMessageContent(List<Chunk> content)
|
||||||
/// URLRegex returns a regex object that matches URLs like:
|
|
||||||
/// - https://example.com
|
|
||||||
/// - http://example.com
|
|
||||||
/// - www.example.com
|
|
||||||
/// - https://sub.example.com
|
|
||||||
/// - example.com
|
|
||||||
/// - sub.example.com
|
|
||||||
///
|
|
||||||
/// It matches URLs with www. or https:// prefix, and also matches URLs
|
|
||||||
/// without a prefix on specific TLDs.
|
|
||||||
/// </summary>
|
|
||||||
private static Regex URLRegex = new(
|
|
||||||
@"((https?:\/\/|www\.)[a-z0-9-]+(\.[a-z0-9-]+)*|([a-z0-9-]+(\.[a-z0-9-]+)*\.(com|net|org|co|io|app)))(:[\d]{1,5})?(\/[^\s]+)?",
|
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
|
||||||
);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds all URL strings in all TextChunks, splits the parent TextChunk
|
|
||||||
/// apart and inserts a new TextChunk with a URIPayload.
|
|
||||||
/// </summary>
|
|
||||||
private List<Chunk> ReplaceContentURLs(List<Chunk> content)
|
|
||||||
{
|
{
|
||||||
var newChunks = new List<Chunk>();
|
var newChunks = new List<Chunk>();
|
||||||
void AddChunkWithMessage(Chunk chunk)
|
void AddChunkWithMessage(Chunk chunk)
|
||||||
@@ -171,42 +151,65 @@ internal class Message
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all URLs with the regex and insert a new TextChunk with a
|
// We replace every emote before checking for URLs
|
||||||
// URIPayload.
|
var builder = new StringBuilder();
|
||||||
var matches = URLRegex.Matches(text.Content);
|
foreach (var word in text.Content.Split(" "))
|
||||||
var remainderIndex = 0;
|
|
||||||
foreach (Match match in matches.Cast<Match>())
|
|
||||||
{
|
{
|
||||||
// Add the text before the URL.
|
if (EmoteCache.Exists(word))
|
||||||
if (match.Index > remainderIndex)
|
|
||||||
{
|
{
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, text.Content[remainderIndex..match.Index]));
|
// We add all the previous collected text parts
|
||||||
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, builder.ToString()));
|
||||||
|
builder.Clear();
|
||||||
|
|
||||||
|
newChunks.Add(new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), "Cool BetterTTV"));
|
||||||
|
builder.Append(' ');
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the remainder index.
|
if (URLRegex.IsMatch(word))
|
||||||
remainderIndex = match.Index + match.Length;
|
{
|
||||||
|
// We add all the previous collected text parts
|
||||||
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, builder.ToString()));
|
||||||
|
builder.Clear();
|
||||||
|
|
||||||
// Create a new TextChunk with a URIPayload for the URL text.
|
// Create a new TextChunk with a URIPayload for the URL text.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var link = UriPayload.ResolveURI(match.Value);
|
var link = UriPayload.ResolveURI(word);
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, match.Value));
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, word));
|
||||||
|
builder.Append(' ');
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
catch (UriFormatException)
|
catch (UriFormatException)
|
||||||
{
|
{
|
||||||
Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{match.Value}'");
|
Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{word}'");
|
||||||
// If the URL is invalid, set the remainder index to the
|
|
||||||
// beginning of the match so it'll get included in the next
|
|
||||||
// regular text chunk.
|
|
||||||
remainderIndex = match.Index;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the text after the last URL.
|
builder.Append($"{word} ");
|
||||||
if (remainderIndex < text.Content.Length)
|
}
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, null, text.Content[remainderIndex..]));
|
// We add the leftovers
|
||||||
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, builder.ToString()[..^1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return newChunks;
|
return newChunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URLRegex returns a regex object that matches URLs like:
|
||||||
|
/// - https://example.com
|
||||||
|
/// - http://example.com
|
||||||
|
/// - www.example.com
|
||||||
|
/// - https://sub.example.com
|
||||||
|
/// - example.com
|
||||||
|
/// - sub.example.com
|
||||||
|
///
|
||||||
|
/// It matches URLs with www. or https:// prefix, and also matches URLs
|
||||||
|
/// without a prefix on specific TLDs.
|
||||||
|
/// </summary>
|
||||||
|
private static Regex URLRegex = new(
|
||||||
|
@"((https?:\/\/|www\.)[a-z0-9-]+(\.[a-z0-9-]+)*|([a-z0-9-]+(\.[a-z0-9-]+)*\.(com|net|org|co|io|app)))(:[\d]{1,5})?(\/[^\s]+)?",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ internal enum PayloadMessagePackType : byte
|
|||||||
Achievement,
|
Achievement,
|
||||||
PartyFinder,
|
PartyFinder,
|
||||||
Uri,
|
Uri,
|
||||||
|
Emote,
|
||||||
Other = 255,
|
Other = 255,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +57,10 @@ public class PayloadMessagePackFormatter : IMessagePackFormatter<Payload?>
|
|||||||
writer.WriteUInt8((byte)PayloadMessagePackType.Uri);
|
writer.WriteUInt8((byte)PayloadMessagePackType.Uri);
|
||||||
writer.WriteString(Encoding.UTF8.GetBytes(uriPayload.Uri.ToString()));
|
writer.WriteString(Encoding.UTF8.GetBytes(uriPayload.Uri.ToString()));
|
||||||
break;
|
break;
|
||||||
|
case EmotePayload emotePayload:
|
||||||
|
writer.WriteUInt8((byte)PayloadMessagePackType.Emote);
|
||||||
|
writer.WriteString(Encoding.UTF8.GetBytes(emotePayload.Code));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
writer.WriteUInt8((byte)PayloadMessagePackType.Other);
|
writer.WriteUInt8((byte)PayloadMessagePackType.Other);
|
||||||
writer.Write(value.Encode());
|
writer.Write(value.Encode());
|
||||||
@@ -80,6 +85,8 @@ public class PayloadMessagePackFormatter : IMessagePackFormatter<Payload?>
|
|||||||
return new PartyFinderPayload(reader.ReadUInt32());
|
return new PartyFinderPayload(reader.ReadUInt32());
|
||||||
case PayloadMessagePackType.Uri:
|
case PayloadMessagePackType.Uri:
|
||||||
return new UriPayload(new Uri(reader.ReadString() ?? ""));
|
return new UriPayload(new Uri(reader.ReadString() ?? ""));
|
||||||
|
case PayloadMessagePackType.Emote:
|
||||||
|
return EmotePayload.ResolveEmote(reader.ReadString() ?? "");
|
||||||
case PayloadMessagePackType.Other:
|
case PayloadMessagePackType.Other:
|
||||||
default:
|
default:
|
||||||
var bytes = reader.ReadBytes() ?? new ReadOnlySequence<byte>();
|
var bytes = reader.ReadBytes() ?? new ReadOnlySequence<byte>();
|
||||||
|
|||||||
Generated
+18
@@ -2093,6 +2093,24 @@ namespace ChatTwo.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Keeps the input focus, even if you enter battle or do other actions.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_KeepInputFocus_Description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_KeepInputFocus_Description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Keep input focus.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_KeepInputFocus_Name {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_KeepInputFocus_Name", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to The way in which {0} should handle keybinds..
|
/// Looks up a localized string similar to The way in which {0} should handle keybinds..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1012,4 +1012,10 @@
|
|||||||
<data name="Options_AdjustPosition_Warning" xml:space="preserve">
|
<data name="Options_AdjustPosition_Warning" xml:space="preserve">
|
||||||
<value>Attention, this change applies immediately and is not discardable!</value>
|
<value>Attention, this change applies immediately and is not discardable!</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_KeepInputFocus_Name" xml:space="preserve">
|
||||||
|
<value>Keep input focus</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||||
|
<value>Keeps the input focus, even if you enter battle or do other actions</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
+29
-12
@@ -122,18 +122,6 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip);
|
Plugin.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PreDraw()
|
|
||||||
{
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void PostDraw()
|
|
||||||
{
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip);
|
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip);
|
||||||
@@ -474,6 +462,21 @@ public sealed class ChatLogWindow : Window
|
|||||||
return !IsHidden;
|
return !IsHidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void PreDraw()
|
||||||
|
{
|
||||||
|
if (Plugin.Config.KeepInputFocus && Activate)
|
||||||
|
ImGui.SetWindowFocus(WindowName);
|
||||||
|
|
||||||
|
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||||
|
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PostDraw()
|
||||||
|
{
|
||||||
|
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||||
|
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
DrawChatLog();
|
DrawChatLog();
|
||||||
@@ -1496,6 +1499,20 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (chunk is not TextChunk text)
|
if (chunk is not TextChunk text)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (chunk.Link?.Type == (PayloadType)0x53)
|
||||||
|
{
|
||||||
|
var emoteSize = ImGui.CalcTextSize("W");
|
||||||
|
emoteSize = emoteSize with { Y = emoteSize.X } * 1.5f;
|
||||||
|
|
||||||
|
var emotePayload = (EmotePayload) chunk.Link;
|
||||||
|
var image = EmoteCache.GetEmote(emotePayload.Code);
|
||||||
|
if (image is { IsLoaded: true })
|
||||||
|
image.Draw(emoteSize);
|
||||||
|
else
|
||||||
|
ImGui.Dummy(emoteSize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var colour = text.Foreground;
|
var colour = text.Foreground;
|
||||||
if (colour == null && text.FallbackColour != null)
|
if (colour == null && text.FallbackColour != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ internal sealed class ChatLog : ISettingsTab
|
|||||||
{
|
{
|
||||||
ImGui.PushTextWrapPos();
|
ImGui.PushTextWrapPos();
|
||||||
|
|
||||||
|
ImGuiUtil.OptionCheckbox(ref Mutable.KeepInputFocus, Language.Options_KeepInputFocus_Name, Language.Options_KeepInputFocus_Description);
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
ImGuiUtil.OptionCheckbox(ref Mutable.PlaySounds, Language.Options_PlaySounds_Name, Language.Options_PlaySounds_Description);
|
ImGuiUtil.OptionCheckbox(ref Mutable.PlaySounds, Language.Options_PlaySounds_Name, Language.Options_PlaySounds_Description);
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
|||||||
@@ -85,3 +85,25 @@ internal class UriPayload(Uri uri) : Payload
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class EmotePayload : Payload
|
||||||
|
{
|
||||||
|
public override PayloadType Type => (PayloadType) 0x53;
|
||||||
|
|
||||||
|
public string Code;
|
||||||
|
|
||||||
|
public static EmotePayload ResolveEmote(string code)
|
||||||
|
{
|
||||||
|
return new EmotePayload { Code = code };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override byte[] EncodeImpl()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,6 +52,12 @@
|
|||||||
"SharpDX.DXGI": "4.2.0"
|
"SharpDX.DXGI": "4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SixLabors.ImageSharp": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.1.4, )",
|
||||||
|
"resolved": "3.1.4",
|
||||||
|
"contentHash": "lFIdxgGDA5iYkUMRFOze7BGLcdpoLFbR+a20kc1W7NepvzU7ejtxtWOg9RvgG7kb9tBoJ3ONYOK6kLil/dgF1w=="
|
||||||
|
},
|
||||||
"XivCommon": {
|
"XivCommon": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[9.0.0, )",
|
"requested": "[9.0.0, )",
|
||||||
|
|||||||
Reference in New Issue
Block a user