From 5b44e66e634b706cc85b12bf3a203adb28090a86 Mon Sep 17 00:00:00 2001 From: Jay Long Date: Tue, 17 Feb 2026 15:30:29 -0600 Subject: [PATCH 01/70] Updating the Titles on Moderation Report Dialogs. --- src/PlayerManagement/TailgrabPanel.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PlayerManagement/TailgrabPanel.xaml b/src/PlayerManagement/TailgrabPanel.xaml index f38b324..133ed60 100644 --- a/src/PlayerManagement/TailgrabPanel.xaml +++ b/src/PlayerManagement/TailgrabPanel.xaml @@ -508,7 +508,7 @@ Title="Tailgrab Player Panel" Date: Tue, 17 Feb 2026 19:10:37 -0600 Subject: [PATCH 02/70] Migrate IsBOS in Avatar/Group to AlertTypeEnum Levels, Turn off Debug, Rename Common.cs to CommonConst.cs --- src/AvatarManagement/AvatarManagement.cs | 11 +- src/Clients/Ollama/Ollama.cs | 41 +-- src/Clients/VRChat/VRChat.cs | 94 +++---- src/Common/AlertTypeEnum.cs | 10 + src/Common/{Common.cs => CommonConst.cs} | 2 +- src/Common/ConfigStore.cs | 10 +- src/Common/SoundManager.cs | 9 + src/Models/AvatarInfo.cs | 5 +- src/Models/GroupInfo.cs | 5 +- src/NLog.config | 2 +- src/PlayerManagement/AvatarCollection.cs | 26 +- src/PlayerManagement/GroupCollection.cs | 25 +- src/PlayerManagement/TailgrabPanel.xaml | 18 +- src/PlayerManagement/TailgrabPanel.xaml.cs | 254 +++++++++--------- src/Program.cs | 18 +- src/configuration/AvatarBosGistListManager.cs | 12 +- src/configuration/GroupBosGistListManager.cs | 12 +- tailgrab.csproj | 6 +- 18 files changed, 296 insertions(+), 264 deletions(-) create mode 100644 src/Common/AlertTypeEnum.cs rename src/Common/{Common.cs => CommonConst.cs} (98%) diff --git a/src/AvatarManagement/AvatarManagement.cs b/src/AvatarManagement/AvatarManagement.cs index 89d8992..b7c13f8 100644 --- a/src/AvatarManagement/AvatarManagement.cs +++ b/src/AvatarManagement/AvatarManagement.cs @@ -129,7 +129,8 @@ public void GetAvatarsFromUser(string userId, string avatarName) ImageUrl = avatar.ImageUrl, CreatedAt = avatar.CreatedAt, UpdatedAt = DateTime.UtcNow, - IsBos = false + IsBos = false, + AlertType = AlertTypeEnum.None }; AddAvatar(avatarInfo); @@ -159,14 +160,14 @@ public void CompactDatabase() internal bool CheckAvatarByName(string avatarName) { var bannedAvatars = _serviceRegistry.GetDBContext().AvatarInfos - .Where(b => b.AvatarName != null && b.AvatarName.Equals(avatarName) && b.IsBos) - .OrderBy(b => b.CreatedAt) + .Where(b => b.AvatarName != null && b.AvatarName.Equals(avatarName) && b.AlertType > 0) + .OrderByDescending(b => b.AlertType) .ToList(); if (bannedAvatars.Count > 0) { - string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Avatar) ?? "Hand"; - SoundManager.PlaySound(soundSetting); + AlertTypeEnum maxAlertType = bannedAvatars[0].AlertType; + SoundManager.PlayAlertSound(CommonConst.Registry_Alert_Avatar, maxAlertType); return true; } diff --git a/src/Clients/Ollama/Ollama.cs b/src/Clients/Ollama/Ollama.cs index 11e5e9c..65709a8 100644 --- a/src/Clients/Ollama/Ollama.cs +++ b/src/Clients/Ollama/Ollama.cs @@ -85,7 +85,7 @@ public void CheckUserProfile(string userId) public static async Task ProfileCheckTask(ConcurrentPriorityQueue, int> priorityQueue, ServiceRegistry serviceRegistry) { - string? ollamaCloudKey = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Key); + string? ollamaCloudKey = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Key); OllamaApiClient? ollamaApi = null; if (ollamaCloudKey is null) { @@ -93,12 +93,12 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue GetUserGroupInformation(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, List userGroups, QueuedProcess item) { logger.Debug($"Processing User Group subscription for userId: {item.UserId}"); - bool isSuspectGroup = false; - string? watchedGroups = string.Empty; + AlertTypeEnum maxAlertType = AlertTypeEnum.None; + string ? watchedGroups = string.Empty; foreach (LimitedUserGroups group in userGroups) { GroupInfo? groupInfo = dBContext.GroupInfos.Find(group.GroupId); @@ -177,7 +177,7 @@ private async static Task GetUserGroupInformation(ServiceRegistry serviceR { GroupId = group.GroupId, GroupName = group.Name, - IsBos = false, + AlertType = AlertTypeEnum.None, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; @@ -190,15 +190,18 @@ private async static Task GetUserGroupInformation(ServiceRegistry serviceR dBContext.GroupInfos.Update(groupInfo); dBContext.SaveChanges(); - if (groupInfo.IsBos) + if (groupInfo.AlertType > AlertTypeEnum.None) { watchedGroups = string.Concat( watchedGroups, " " + groupInfo.GroupName ); - isSuspectGroup = true; + if( groupInfo.AlertType > maxAlertType) + { + maxAlertType = groupInfo.AlertType; + } } } } - if (isSuspectGroup) + if (maxAlertType > AlertTypeEnum.None) { Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId ?? string.Empty); if (player != null) @@ -208,20 +211,20 @@ private async static Task GetUserGroupInformation(ServiceRegistry serviceR serviceRegistry.GetPlayerManager().AddPlayerEventByUserId(item.UserId ?? string.Empty, PlayerEvent.EventType.GroupWatch, $"User is member of watched group(s): {watchedGroups}"); } - string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Group) ?? "Hand"; - SoundManager.PlaySound(soundSetting); + SoundManager.PlayAlertSound(CommonConst.Registry_Alert_Group, maxAlertType); + return true; } - return isSuspectGroup; + return false; } private async static void GetEvaluationFromCloud(OllamaApiClient ollamaApi, ServiceRegistry serviceRegistry, QueuedProcess item ) { - string? ollamaPrompt = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Prompt); + string? ollamaPrompt = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Prompt); GenerateRequest request = new GenerateRequest { Model = ollamaApi.SelectedModel, - Prompt = string.Concat( ollamaPrompt ?? Tailgrab.Common.Common.Default_Ollama_API_Prompt, item.UserBio ?? string.Empty ), + Prompt = string.Concat( ollamaPrompt ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Prompt, item.UserBio ?? string.Empty ), Stream = false }; @@ -343,14 +346,14 @@ private static bool CheckLines(string input, string knownString) try { - string? ollamaCloudKey = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Key); + string? ollamaCloudKey = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Key); if (ollamaCloudKey == null) { logger.Warn("Ollama API credentials are not set"); return null; } - string ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.Common.Default_Ollama_API_Endpoint; + string ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Endpoint; ImageReference? imageReference = await _serviceRegistry.GetVRChatAPIClient().GetImageReference(assetId, userId, imageUrlList); if (imageReference != null) @@ -367,14 +370,14 @@ private static bool CheckLines(string input, string knownString) using (OllamaApiClient ollamaApi = new OllamaApiClient(ollamaHttpClient)) { - string? ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Model) ?? Tailgrab.Common.Common.Default_Ollama_API_Model; + string? ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Model) ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Model; ollamaApi.SelectedModel = ollamaModel; - string? ollamaPrompt = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Image_Prompt); + string? ollamaPrompt = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Image_Prompt); GenerateRequest request = new GenerateRequest { Model = ollamaApi.SelectedModel, - Prompt = ollamaPrompt ?? Tailgrab.Common.Common.Default_Ollama_API_Image_Prompt, + Prompt = ollamaPrompt ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Image_Prompt, Images = imageReference.Base64Data.ToArray(), Stream = false }; diff --git a/src/Clients/VRChat/VRChat.cs b/src/Clients/VRChat/VRChat.cs index 02e0a6d..c5377d6 100644 --- a/src/Clients/VRChat/VRChat.cs +++ b/src/Clients/VRChat/VRChat.cs @@ -16,16 +16,16 @@ namespace Tailgrab.Clients.VRChat public class VRChatClient { private const string URI_VRC_BASE_API = "https://api.vrchat.cloud"; - public static string UserAgent = "Tailgrab/1.0.7"; + public static string UserAgent = "Tailgrab/1.1.0"; public static Logger logger = LogManager.GetCurrentClassLogger(); private IVRChat? _vrchat; public async Task Initialize() { - string? username = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_UserName); - string? password = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_Password); - string? twoFactorSecret = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_2FactorKey); + string? username = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_UserName); + string? password = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_Password); + string? twoFactorSecret = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_2FactorKey); if (username is null || password is null || twoFactorSecret is null) { @@ -529,6 +529,50 @@ internal async Task SubmitModerationReportAsync(ModerationReportPayload rp } } + #region Non Public Helper Types + private class SerializableCookie + { + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Domain { get; set; } = string.Empty; + public string Path { get; set; } = "/"; + public DateTime Expires { get; set; } = DateTime.MinValue; + public bool Secure { get; set; } + public bool HttpOnly { get; set; } + + public Cookie ToCookie() + { + var cookie = new Cookie(Name, Value, Path, Domain) + { + Secure = Secure, + HttpOnly = HttpOnly + }; + + if (Expires != DateTime.MinValue) + { + cookie.Expires = Expires; + } + + return cookie; + } + + public static SerializableCookie FromCookie(Cookie c) + { + return new SerializableCookie + { + Name = c.Name, + Value = c.Value, + Domain = c.Domain ?? string.Empty, + Path = c.Path ?? "/", + Expires = c.Expires, + Secure = c.Secure, + HttpOnly = c.HttpOnly + }; + } + } + #endregion + + #region Non Public JSON Serializable Types public class AvatarModerationItem { [JsonProperty("avatarModerationType")] @@ -682,47 +726,6 @@ public class ModerationReportDetails public string HolderId { get; set; } = string.Empty; } - private class SerializableCookie - { - public string Name { get; set; } = string.Empty; - public string Value { get; set; } = string.Empty; - public string Domain { get; set; } = string.Empty; - public string Path { get; set; } = "/"; - public DateTime Expires { get; set; } = DateTime.MinValue; - public bool Secure { get; set; } - public bool HttpOnly { get; set; } - - public Cookie ToCookie() - { - var cookie = new Cookie(Name, Value, Path, Domain) - { - Secure = Secure, - HttpOnly = HttpOnly - }; - - if (Expires != DateTime.MinValue) - { - cookie.Expires = Expires; - } - - return cookie; - } - - public static SerializableCookie FromCookie(Cookie c) - { - return new SerializableCookie - { - Name = c.Name, - Value = c.Value, - Domain = c.Domain ?? string.Empty, - Path = c.Path ?? "/", - Expires = c.Expires, - Secure = c.Secure, - HttpOnly = c.HttpOnly - }; - } - } - public class PrintInfo { [JsonProperty("authorId")] @@ -754,5 +757,6 @@ public class PrintFileInfo [JsonProperty("image")] public string ImageUrl { get; set; } = string.Empty; } + #endregion } } diff --git a/src/Common/AlertTypeEnum.cs b/src/Common/AlertTypeEnum.cs new file mode 100644 index 0000000..736b7bc --- /dev/null +++ b/src/Common/AlertTypeEnum.cs @@ -0,0 +1,10 @@ +namespace Tailgrab.Common +{ + public enum AlertTypeEnum + { + None = 0, + Watch = 1, + Nuisance = 2, + Crasher = 3 + } +} diff --git a/src/Common/Common.cs b/src/Common/CommonConst.cs similarity index 98% rename from src/Common/Common.cs rename to src/Common/CommonConst.cs index 77b2438..db4a505 100644 --- a/src/Common/Common.cs +++ b/src/Common/CommonConst.cs @@ -1,6 +1,6 @@ namespace Tailgrab.Common { - public static class Common + public static class CommonConst { public const string ApplicationName = "Tailgrab"; public const string CompanyName = "DeviousFox"; diff --git a/src/Common/ConfigStore.cs b/src/Common/ConfigStore.cs index 9aa263c..b929ed0 100644 --- a/src/Common/ConfigStore.cs +++ b/src/Common/ConfigStore.cs @@ -18,7 +18,7 @@ public static void SaveSecret(string name, string value) var protectedBytes = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser); var base64 = Convert.ToBase64String(protectedBytes); - using (var key = Registry.CurrentUser.CreateSubKey(Common.ConfigRegistryPath)) + using (var key = Registry.CurrentUser.CreateSubKey(CommonConst.ConfigRegistryPath)) { key.SetValue(name, base64, RegistryValueKind.String); } @@ -28,7 +28,7 @@ public static void SaveSecret(string name, string value) { if (name == null) throw new ArgumentNullException(nameof(name)); - using (var key = Registry.CurrentUser.OpenSubKey(Common.ConfigRegistryPath)) + using (var key = Registry.CurrentUser.OpenSubKey(CommonConst.ConfigRegistryPath)) { if (key == null) return null; var base64 = key.GetValue(name) as string; @@ -48,7 +48,7 @@ public static void SaveSecret(string name, string value) public static void DeleteSecret(string name) { - using (var key = Registry.CurrentUser.OpenSubKey(Common.ConfigRegistryPath, writable: true)) + using (var key = Registry.CurrentUser.OpenSubKey(CommonConst.ConfigRegistryPath, writable: true)) { if (key == null) return; key.DeleteValue(name, throwOnMissingValue: false); @@ -59,7 +59,7 @@ public static void DeleteSecret(string name) { try { - using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.ConfigRegistryPath)) + using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(CommonConst.ConfigRegistryPath)) { if (key == null) { @@ -88,7 +88,7 @@ public static void PutStoredUri(string keyName, string keyValue) { try { - using (RegistryKey key = Registry.CurrentUser.CreateSubKey(Common.ConfigRegistryPath)) + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(CommonConst.ConfigRegistryPath)) { key.SetValue(keyName, keyValue, RegistryValueKind.String); } diff --git a/src/Common/SoundManager.cs b/src/Common/SoundManager.cs index 248523f..e0ffc3a 100644 --- a/src/Common/SoundManager.cs +++ b/src/Common/SoundManager.cs @@ -56,6 +56,15 @@ public static List GetAvailableSounds() } } + public static void PlayAlertSound(string entityType, AlertTypeEnum alertType ) + { + string? soundSetting = ConfigStore.LoadSecret(entityType) ?? "Hand"; + for (int i = 0; i < (int)alertType; i++) + { + PlaySound(soundSetting); + } + } + /// /// Play a system alert sound or a file under the local "sounds" directory. /// Recognised system names (case-insensitive): Asterisk, Beep, Exclamation, Hand, Question diff --git a/src/Models/AvatarInfo.cs b/src/Models/AvatarInfo.cs index c077ea5..e7e5f39 100644 --- a/src/Models/AvatarInfo.cs +++ b/src/Models/AvatarInfo.cs @@ -2,6 +2,7 @@ #nullable disable using System; using System.Collections.Generic; +using Tailgrab.Common; namespace Tailgrab.Models; @@ -19,6 +20,8 @@ public partial class AvatarInfo public bool IsBos { get; set; } + public AlertTypeEnum AlertType { get; set; } = AlertTypeEnum.None; + public string ImageUrl { get; set; } public AvatarInfo() @@ -28,6 +31,6 @@ public AvatarInfo() public override string ToString() { - return $"GroupId: {AvatarId}, UserId: {UserId}, GroupName: {AvatarName}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}, IsBOS: {IsBos}"; + return $"GroupId: {AvatarId}, UserId: {UserId}, GroupName: {AvatarName}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}, IsBOS: {IsBos}, AlertType: {AlertType}"; } } \ No newline at end of file diff --git a/src/Models/GroupInfo.cs b/src/Models/GroupInfo.cs index 28b6ece..2a90954 100644 --- a/src/Models/GroupInfo.cs +++ b/src/Models/GroupInfo.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http.HttpResults; using System; using System.Collections.Generic; +using Tailgrab.Common; using VRChat.API.Model; namespace Tailgrab.Models; @@ -15,6 +16,8 @@ public partial class GroupInfo public bool IsBos{ get; set; } + public AlertTypeEnum AlertType { get; set; } = AlertTypeEnum.None; + public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } @@ -26,6 +29,6 @@ public GroupInfo() public override string ToString() { - return $"GroupInfo - GroupId: {GroupId}, GroupName: {GroupName}, IsBOS: {IsBos}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}"; + return $"GroupId: {GroupId}, GroupName: {GroupName}, IsBOS: {IsBos}, AlertType: {AlertType}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}"; } } diff --git a/src/NLog.config b/src/NLog.config index b15b72e..5ec21a3 100644 --- a/src/NLog.config +++ b/src/NLog.config @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/src/PlayerManagement/AvatarCollection.cs b/src/PlayerManagement/AvatarCollection.cs index a471ece..4e86fe6 100644 --- a/src/PlayerManagement/AvatarCollection.cs +++ b/src/PlayerManagement/AvatarCollection.cs @@ -1,6 +1,7 @@ using System.Collections.Specialized; using System.ComponentModel; using Microsoft.EntityFrameworkCore; +using Tailgrab.Common; namespace Tailgrab.PlayerManagement { @@ -152,39 +153,34 @@ public System.Collections.IEnumerator GetEnumerator() public class AvatarInfoViewModel : INotifyPropertyChanged { + private AlertTypeEnum _alertType; + public string AvatarId { get; set; } public string AvatarName { get; set; } - private bool _isBos; - public bool IsBos + + public AlertTypeEnum AlertType { - get => _isBos; + get => _alertType; set { - if (_isBos != value) + if (_alertType != value) { - _isBos = value; - IsBosText = BoolToYesNo(_isBos); - OnPropertyChanged(nameof(IsBos)); - OnPropertyChanged(nameof(IsBosText)); + _alertType = value; + OnPropertyChanged(nameof(AlertType)); } } } - - public string IsBosText { get; set; } + public DateTime? UpdatedAt { get; set; } public AvatarInfoViewModel(Tailgrab.Models.AvatarInfo a) { AvatarId = a.AvatarId; AvatarName = a.AvatarName; - IsBos = a.IsBos; UpdatedAt = a.UpdatedAt; - IsBosText = BoolToYesNo(IsBos); + AlertType = a.AlertType; } - // Convert boolean to YES/NO string for display - public static string BoolToYesNo(bool value) => value ? "YES" : "NO"; - public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) { diff --git a/src/PlayerManagement/GroupCollection.cs b/src/PlayerManagement/GroupCollection.cs index efb9bbd..9ff80d2 100644 --- a/src/PlayerManagement/GroupCollection.cs +++ b/src/PlayerManagement/GroupCollection.cs @@ -1,6 +1,7 @@ using System.Collections.Specialized; using System.ComponentModel; using Microsoft.EntityFrameworkCore; +using Tailgrab.Common; namespace Tailgrab.PlayerManagement { @@ -154,38 +155,30 @@ public System.Collections.IEnumerator GetEnumerator() public class GroupInfoViewModel : INotifyPropertyChanged { public string GroupId { get; set; } - public string GroupName { get; set; } - private bool _isBos; - public bool IsBos + public string GroupName { get; set; } + private AlertTypeEnum _AlertType; + public AlertTypeEnum AlertType { - get => _isBos; + get => _AlertType; set { - if (_isBos != value) + if (_AlertType != value) { - _isBos = value; - IsBosText = BoolToYesNo(_isBos); - OnPropertyChanged(nameof(IsBos)); - OnPropertyChanged(nameof(IsBosText)); + _AlertType = value; + OnPropertyChanged(nameof(AlertType)); } } } - - public string IsBosText { get; set; } public DateTime? UpdatedAt { get; set; } public GroupInfoViewModel(Tailgrab.Models.GroupInfo a) { GroupId = a.GroupId; GroupName = a.GroupName; - IsBos = a.IsBos; + AlertType = a.AlertType; UpdatedAt = a.UpdatedAt; - IsBosText = BoolToYesNo(IsBos); } - // Convert boolean to YES/NO string for display - public static string BoolToYesNo(bool value) => value ? "YES" : "NO"; - public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) { diff --git a/src/PlayerManagement/TailgrabPanel.xaml b/src/PlayerManagement/TailgrabPanel.xaml index 133ed60..5251075 100644 --- a/src/PlayerManagement/TailgrabPanel.xaml +++ b/src/PlayerManagement/TailgrabPanel.xaml @@ -891,18 +891,18 @@ Title="Tailgrab Player Panel" - - + + - + - @@ -966,17 +966,17 @@ Title="Tailgrab Player Panel" - + - + - diff --git a/src/PlayerManagement/TailgrabPanel.xaml.cs b/src/PlayerManagement/TailgrabPanel.xaml.cs index 8fb647d..d4f39aa 100644 --- a/src/PlayerManagement/TailgrabPanel.xaml.cs +++ b/src/PlayerManagement/TailgrabPanel.xaml.cs @@ -124,7 +124,19 @@ public string ElapsedTime public PlayerViewModel? SelectedPast { get; set; } public static Logger logger = LogManager.GetCurrentClassLogger(); - public List> IsBosOptions { get; set; } + public List> IsBosOptions { get; } = new List> + { + new KeyValuePair("YES", true), + new KeyValuePair("NO", false) + }; + + public List> AlertTypeOptions { get; } = new List> + { + new KeyValuePair("None", AlertTypeEnum.None), + new KeyValuePair("Watch", AlertTypeEnum.Watch), + new KeyValuePair("Nuisance", AlertTypeEnum.Nuisance), + new KeyValuePair("Crasher", AlertTypeEnum.Crasher) + }; protected ServiceRegistry _serviceRegistry; @@ -159,25 +171,18 @@ public TailgrabPanel(ServiceRegistry serviceRegistry) // User collection ordered by DisplayName at source UserDbView.SortDescriptions.Add(new SortDescription("DisplayName", ListSortDirection.Ascending)); - // Options for the IsBOS combo column - IsBosOptions = new List> - { - new KeyValuePair("YES", true), - new KeyValuePair("NO", false) - }; - #region Secret Config Load // Load saved secrets into UI fields if desired (not displayed in this view directly) - var vrUser = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_UserName); - var vrPass = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_Password); - var vr2fa = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_2FactorKey); - var ollamaKey = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Key); - var ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.Common.Default_Ollama_API_Endpoint; - var ollamaProfilePrompt = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Prompt) ?? Tailgrab.Common.Common.Default_Ollama_API_Prompt; - var ollamaImagePrompt = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Image_Prompt) ?? Tailgrab.Common.Common.Default_Ollama_API_Image_Prompt; - var ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Model) ?? Tailgrab.Common.Common.Default_Ollama_API_Model; - var avatarGistUri = ConfigStore.GetStoredUri(Tailgrab.Common.Common.Registry_Avatar_Gist); - var groupGistUri = ConfigStore.GetStoredUri(Tailgrab.Common.Common.Registry_Group_Gist); + var vrUser = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_UserName); + var vrPass = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_Password); + var vr2fa = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_2FactorKey); + var ollamaKey = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Key); + var ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Endpoint; + var ollamaProfilePrompt = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Prompt) ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Prompt; + var ollamaImagePrompt = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Image_Prompt) ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Image_Prompt; + var ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Model) ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Model; + var avatarGistUri = ConfigStore.GetStoredUri(Tailgrab.Common.CommonConst.Registry_Avatar_Gist); + var groupGistUri = ConfigStore.GetStoredUri(Tailgrab.Common.CommonConst.Registry_Group_Gist); // Populate UI boxes but do not reveal secrets if (!string.IsNullOrEmpty(vrUser)) VrUserBox.Text = vrUser; @@ -201,9 +206,9 @@ public TailgrabPanel(ServiceRegistry serviceRegistry) ProfileAlertCombo.ItemsSource = sounds; // Load saved registry values into selected items - var avatar = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Alert_Avatar); - var group = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Alert_Group); - var profile = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Alert_Profile); + var avatar = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Alert_Avatar); + var group = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Alert_Group); + var profile = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_Alert_Profile); if (!string.IsNullOrEmpty(avatar)) AvatarAlertCombo.SelectedItem = avatar; if (!string.IsNullOrEmpty(group)) GroupAlertCombo.SelectedItem = group; @@ -244,44 +249,44 @@ private void SaveConfig_Click(object sender, RoutedEventArgs e) try { // Save to registry protected store - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_VRChat_Web_UserName, VrUserBox.Text ?? string.Empty); - if (!string.IsNullOrEmpty(VrPassBox.Password)) ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_VRChat_Web_Password, VrPassBox.Password); - if (!string.IsNullOrEmpty(Vr2FaBox.Password)) ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_VRChat_Web_2FactorKey, Vr2FaBox.Password); - if (!string.IsNullOrEmpty(VrOllamaBox.Password)) ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Ollama_API_Key, VrOllamaBox.Password); - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint, VrOllamaEndpointBox.Text ?? Tailgrab.Common.Common.Default_Ollama_API_Endpoint); - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Ollama_API_Prompt, VrOllamaPromptBox.Text ?? Tailgrab.Common.Common.Default_Ollama_API_Prompt); - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Ollama_API_Image_Prompt, VrOllamaImagePromptBox.Text ?? Tailgrab.Common.Common.Default_Ollama_API_Image_Prompt); - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Ollama_API_Model, VrOllamaModelBox.Text ?? Tailgrab.Common.Common.Default_Ollama_API_Model); - - ConfigStore.PutStoredUri(Common.Common.Registry_Avatar_Gist, avatarGistUrl.Text); - ConfigStore.PutStoredUri(Common.Common.Registry_Group_Gist, groupGistUrl.Text); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_UserName, VrUserBox.Text ?? string.Empty); + if (!string.IsNullOrEmpty(VrPassBox.Password)) ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_Password, VrPassBox.Password); + if (!string.IsNullOrEmpty(Vr2FaBox.Password)) ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_2FactorKey, Vr2FaBox.Password); + if (!string.IsNullOrEmpty(VrOllamaBox.Password)) ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Key, VrOllamaBox.Password); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Endpoint, VrOllamaEndpointBox.Text ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Endpoint); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Prompt, VrOllamaPromptBox.Text ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Prompt); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Image_Prompt, VrOllamaImagePromptBox.Text ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Image_Prompt); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Ollama_API_Model, VrOllamaModelBox.Text ?? Tailgrab.Common.CommonConst.Default_Ollama_API_Model); + + ConfigStore.PutStoredUri(Common.CommonConst.Registry_Avatar_Gist, avatarGistUrl.Text); + ConfigStore.PutStoredUri(Common.CommonConst.Registry_Group_Gist, groupGistUrl.Text); // Save alert sound selections (or delete if none) if (AvatarAlertCombo.SelectedItem is string avatarSound && !string.IsNullOrEmpty(avatarSound)) { - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Alert_Avatar, avatarSound); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Alert_Avatar, avatarSound); } else { - ConfigStore.DeleteSecret(Tailgrab.Common.Common.Registry_Alert_Avatar); + ConfigStore.DeleteSecret(Tailgrab.Common.CommonConst.Registry_Alert_Avatar); } if (GroupAlertCombo.SelectedItem is string groupSound && !string.IsNullOrEmpty(groupSound)) { - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Alert_Group, groupSound); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Alert_Group, groupSound); } else { - ConfigStore.DeleteSecret(Tailgrab.Common.Common.Registry_Alert_Group); + ConfigStore.DeleteSecret(Tailgrab.Common.CommonConst.Registry_Alert_Group); } if (ProfileAlertCombo.SelectedItem is string profileSound && !string.IsNullOrEmpty(profileSound)) { - ConfigStore.SaveSecret(Tailgrab.Common.Common.Registry_Alert_Profile, profileSound); + ConfigStore.SaveSecret(Tailgrab.Common.CommonConst.Registry_Alert_Profile, profileSound); } else { - ConfigStore.DeleteSecret(Tailgrab.Common.Common.Registry_Alert_Profile); + ConfigStore.DeleteSecret(Tailgrab.Common.CommonConst.Registry_Alert_Profile); } System.Windows.MessageBox.Show("Configuration saved. Restart the Applicaton for all changes to take affect.", "Config", MessageBoxButton.OK, MessageBoxImage.Information); @@ -332,76 +337,6 @@ private void StatusBarTimer_Tick(object? sender, EventArgs e) } } - private void UserHyperlink_RequestNavigate(object? sender, System.Windows.Navigation.RequestNavigateEventArgs e) - { - try - { - logger.Info($"Opening User URL: {e.Uri}"); - var uri = new Uri($"https://vrchat.com/home/user/{e.Uri}"); - var psi = new System.Diagnostics.ProcessStartInfo(uri.AbsoluteUri) - { - UseShellExecute = true - }; - System.Diagnostics.Process.Start(psi); - } - catch (Exception ex) - { - logger?.Error(ex, "Failed to open user URL"); - } - e.Handled = true; - } - - // User DB UI handlers - private void UserDbRefresh_Click(object sender, RoutedEventArgs e) - { - RefreshUserDb(); - } - - private void UserDbApplyFilter_Click(object sender, RoutedEventArgs e) - { - ApplyUserDbFilter(UserDbView, UserDbFilterBox.Text); - } - - private void UserDbClearFilter_Click(object sender, RoutedEventArgs e) - { - UserDbFilterBox.Text = string.Empty; - ApplyUserDbFilter(UserDbView, string.Empty); - } - - private void ApplyUserDbFilter(ICollectionView view, string filterText) - { - // Push filter to database for better performance - if (string.IsNullOrWhiteSpace(filterText)) - { - UserDbItems.SetFilter(null); - } - else - { - UserDbItems.SetFilter(filterText.Trim()); - } - } - - private void UserDbGrid_CellEditEnding(object sender, System.Windows.Controls.DataGridCellEditEndingEventArgs e) - { - if (e.Row.Item is UserInfoViewModel vm) - { - try - { - var db = _serviceRegistry.GetDBContext(); - var entity = db.UserInfos.Find(vm.UserId); - if (entity != null) - { - entity.IsBos = vm.IsBos; - entity.UpdatedAt = DateTime.UtcNow; - db.UserInfos.Update(entity); - db.SaveChanges(); - vm.UpdatedAt = entity.UpdatedAt; - } - } - catch { } - } - } - private void FallbackTimer_Tick(object? sender, EventArgs e) { // Ensure collections reflect PlayerManager state @@ -577,7 +512,6 @@ private void UpdateCollectionsFromSnapshot(System.Collections.Generic.List= AlertTypeEnum.Nuisance ) { await _serviceRegistry.GetVRChatAPIClient().BlockAvatarGlobal(vm.AvatarId); } else @@ -1785,7 +1719,8 @@ private void AvatarFetch_Click(object sender, RoutedEventArgs e) ImageUrl = avatar.ImageUrl ?? string.Empty, CreatedAt = avatar.CreatedAt, UpdatedAt = DateTime.UtcNow, - IsBos = false + IsBos = false, + AlertType = AlertTypeEnum.None }; dbContext.AvatarInfos.Add(newEntity); dbContext.SaveChanges(); @@ -1877,15 +1812,6 @@ private void RefreshGroupDb() catch { } } - private void RefreshUserDb() - { - try - { - UserDbItems?.Refresh(); - } - catch { } - } - private void GroupDbGrid_CellEditEnding(object sender, System.Windows.Controls.DataGridCellEditEndingEventArgs e) { if (e.Row.Item is GroupInfoViewModel vm) @@ -1896,7 +1822,7 @@ private void GroupDbGrid_CellEditEnding(object sender, System.Windows.Controls.D var entity = db.GroupInfos.Find(vm.GroupId); if (entity != null) { - entity.IsBos = vm.IsBos; + entity.AlertType = vm.AlertType; entity.UpdatedAt = DateTime.UtcNow; db.GroupInfos.Update(entity); db.SaveChanges(); @@ -1946,6 +1872,90 @@ private void GroupHyperlink_RequestNavigate(object? sender, System.Windows.Navig } #endregion + // + // User DB UI handlers + #region User DB handlers + private void UserDbRefresh_Click(object sender, RoutedEventArgs e) + { + RefreshUserDb(); + } + + private void RefreshUserDb() + { + try + { + UserDbItems?.Refresh(); + } + catch { } + } + + private void UserDbApplyFilter_Click(object sender, RoutedEventArgs e) + { + ApplyUserDbFilter(UserDbView, UserDbFilterBox.Text); + } + + private void UserDbClearFilter_Click(object sender, RoutedEventArgs e) + { + UserDbFilterBox.Text = string.Empty; + ApplyUserDbFilter(UserDbView, string.Empty); + } + + private void ApplyUserDbFilter(ICollectionView view, string filterText) + { + // Push filter to database for better performance + if (string.IsNullOrWhiteSpace(filterText)) + { + UserDbItems.SetFilter(null); + } + else + { + UserDbItems.SetFilter(filterText.Trim()); + } + } + + private void UserDbGrid_CellEditEnding(object sender, System.Windows.Controls.DataGridCellEditEndingEventArgs e) + { + if (e.Row.Item is UserInfoViewModel vm) + { + try + { + var db = _serviceRegistry.GetDBContext(); + var entity = db.UserInfos.Find(vm.UserId); + if (entity != null) + { + entity.IsBos = vm.IsBos; + entity.UpdatedAt = DateTime.UtcNow; + db.UserInfos.Update(entity); + db.SaveChanges(); + vm.UpdatedAt = entity.UpdatedAt; + } + } + catch { } + } + } + + private void UserHyperlink_RequestNavigate(object? sender, System.Windows.Navigation.RequestNavigateEventArgs e) + { + try + { + logger.Info($"Opening User URL: {e.Uri}"); + var uri = new Uri($"https://vrchat.com/home/user/{e.Uri}"); + var psi = new System.Diagnostics.ProcessStartInfo(uri.AbsoluteUri) + { + UseShellExecute = true + }; + System.Diagnostics.Process.Start(psi); + } + catch (Exception ex) + { + logger?.Error(ex, "Failed to open user URL"); + } + e.Handled = true; + } + + #endregion + + private void ApplyFilter(ICollectionView view, string filterText) { if (string.IsNullOrWhiteSpace(filterText)) diff --git a/src/Program.cs b/src/Program.cs index 85f9ede..17f691a 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -326,7 +326,7 @@ private static void InitializeMissingRegistryItems() { try { - using var key = Registry.CurrentUser.CreateSubKey(Tailgrab.Common.Common.ConfigRegistryPath); + using var key = Registry.CurrentUser.CreateSubKey(Tailgrab.Common.CommonConst.ConfigRegistryPath); if (key == null) { logger.Warn("Failed to create or open registry key for configuration."); @@ -344,14 +344,14 @@ void SetDefaultIfMissing(string name, string defaultValue) } // Initialize Ollama API registry keys with defaults - SetDefaultIfMissing(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint, - Tailgrab.Common.Common.Default_Ollama_API_Endpoint); - SetDefaultIfMissing(Tailgrab.Common.Common.Registry_Ollama_API_Model, - Tailgrab.Common.Common.Default_Ollama_API_Model); - SetDefaultIfMissing(Tailgrab.Common.Common.Registry_Ollama_API_Prompt, - Tailgrab.Common.Common.Default_Ollama_API_Prompt); - SetDefaultIfMissing(Tailgrab.Common.Common.Registry_Ollama_API_Image_Prompt, - Tailgrab.Common.Common.Default_Ollama_API_Image_Prompt); + SetDefaultIfMissing(Tailgrab.Common.CommonConst.Registry_Ollama_API_Endpoint, + Tailgrab.Common.CommonConst.Default_Ollama_API_Endpoint); + SetDefaultIfMissing(Tailgrab.Common.CommonConst.Registry_Ollama_API_Model, + Tailgrab.Common.CommonConst.Default_Ollama_API_Model); + SetDefaultIfMissing(Tailgrab.Common.CommonConst.Registry_Ollama_API_Prompt, + Tailgrab.Common.CommonConst.Default_Ollama_API_Prompt); + SetDefaultIfMissing(Tailgrab.Common.CommonConst.Registry_Ollama_API_Image_Prompt, + Tailgrab.Common.CommonConst.Default_Ollama_API_Image_Prompt); // Note: The following keys don't have default values and should be set by the user: // - Registry_VRChat_Web_UserName diff --git a/src/configuration/AvatarBosGistListManager.cs b/src/configuration/AvatarBosGistListManager.cs index 9a30e01..a13f952 100644 --- a/src/configuration/AvatarBosGistListManager.cs +++ b/src/configuration/AvatarBosGistListManager.cs @@ -114,7 +114,7 @@ private string CalculateMD5Checksum(string content) { try { - using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.Common.ConfigRegistryPath)) + using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.CommonConst.ConfigRegistryPath)) { if (key == null) { @@ -122,7 +122,7 @@ private string CalculateMD5Checksum(string content) return null; } - string? value = key.GetValue(Common.Common.Registry_Avatar_Checksum) as string; + string? value = key.GetValue(Common.CommonConst.Registry_Avatar_Checksum) as string; if (string.IsNullOrEmpty(value)) { logger.Debug("No checksum stored in registry."); @@ -143,7 +143,7 @@ private string CalculateMD5Checksum(string content) { try { - using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.Common.ConfigRegistryPath)) + using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.CommonConst.ConfigRegistryPath)) { if (key == null) { @@ -151,7 +151,7 @@ private string CalculateMD5Checksum(string content) return null; } - string? value = key.GetValue(Common.Common.Registry_Avatar_Gist) as string; + string? value = key.GetValue(Common.CommonConst.Registry_Avatar_Gist) as string; if (string.IsNullOrEmpty(value)) { logger.Debug("No Avatar GIST Uri stored in registry."); @@ -172,9 +172,9 @@ private void SaveChecksum(string checksum) { try { - using (RegistryKey key = Registry.CurrentUser.CreateSubKey(Common.Common.ConfigRegistryPath)) + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(Common.CommonConst.ConfigRegistryPath)) { - key.SetValue(Common.Common.Registry_Avatar_Checksum, checksum, RegistryValueKind.String); + key.SetValue(Common.CommonConst.Registry_Avatar_Checksum, checksum, RegistryValueKind.String); } } catch (Exception ex) diff --git a/src/configuration/GroupBosGistListManager.cs b/src/configuration/GroupBosGistListManager.cs index 81135f1..62724e3 100644 --- a/src/configuration/GroupBosGistListManager.cs +++ b/src/configuration/GroupBosGistListManager.cs @@ -114,7 +114,7 @@ private string CalculateMD5Checksum(string content) { try { - using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.Common.ConfigRegistryPath)) + using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.CommonConst.ConfigRegistryPath)) { if (key == null) { @@ -122,7 +122,7 @@ private string CalculateMD5Checksum(string content) return null; } - string? value = key.GetValue(Common.Common.Registry_Group_Checksum) as string; + string? value = key.GetValue(Common.CommonConst.Registry_Group_Checksum) as string; if (string.IsNullOrEmpty(value)) { logger.Debug("No checksum stored in registry."); @@ -143,7 +143,7 @@ private string CalculateMD5Checksum(string content) { try { - using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.Common.ConfigRegistryPath)) + using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.CommonConst.ConfigRegistryPath)) { if (key == null) { @@ -151,7 +151,7 @@ private string CalculateMD5Checksum(string content) return null; } - string? value = key.GetValue(Common.Common.Registry_Group_Gist) as string; + string? value = key.GetValue(Common.CommonConst.Registry_Group_Gist) as string; if (string.IsNullOrEmpty(value)) { logger.Debug("No Avatar GIST Uri stored in registry."); @@ -172,9 +172,9 @@ private void SaveChecksum(string checksum) { try { - using (RegistryKey key = Registry.CurrentUser.CreateSubKey(Common.Common.ConfigRegistryPath)) + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(Common.CommonConst.ConfigRegistryPath)) { - key.SetValue(Common.Common.Registry_Group_Checksum, checksum, RegistryValueKind.String); + key.SetValue(Common.CommonConst.Registry_Group_Checksum, checksum, RegistryValueKind.String); } } catch (Exception ex) diff --git a/tailgrab.csproj b/tailgrab.csproj index d003c4f..593f0fc 100644 --- a/tailgrab.csproj +++ b/tailgrab.csproj @@ -7,9 +7,9 @@ true true enable - 1.0.9.1722 - 1.0.9.1722 - 1.0.9.1722 + 1.1.0.1722 + 1.1.0.1722 + 1.1.0.1722 src\Resources\tailgrab_large.ico From 7e7809d1d828cbd566fbbc5c7c33bdee572c6a71 Mon Sep 17 00:00:00 2001 From: Jay Long Date: Wed, 18 Feb 2026 05:03:55 -0600 Subject: [PATCH 03/70] Updates to externalize build number & window title. --- BuildNumber.txt | 1 + src/Models/TailgrabDBContext.cs | 56 ++++++++++++++++++++++ src/PlayerManagement/TailgrabPanel.xaml.cs | 3 ++ tailgrab.csproj | 14 ++++-- 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 BuildNumber.txt diff --git a/BuildNumber.txt b/BuildNumber.txt new file mode 100644 index 0000000..a85618d --- /dev/null +++ b/BuildNumber.txt @@ -0,0 +1 @@ +1722 \ No newline at end of file diff --git a/src/Models/TailgrabDBContext.cs b/src/Models/TailgrabDBContext.cs index 9c70505..f613acc 100644 --- a/src/Models/TailgrabDBContext.cs +++ b/src/Models/TailgrabDBContext.cs @@ -97,4 +97,60 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + + public void UpgradeDatabase() + { + ExecuteSqlTransaction( + "ALTER TABLE AvatarInfo ADD COLUMN alertType INTEGER NOT NULL DEFAULT 0", + "UPDATE AvatarInfo SET alertType = 1 WHERE IsBOS = 1", + "ALTER TABLE GroupInfo ADD COLUMN alertType INTEGER NOT NULL DEFAULT 0", + "UPDATE GroupInfo SET alertType = 1 WHERE IsBOS = 1" + ); + } + + private void ExecuteSql(string sql) + { + Database.ExecuteSqlRaw(sql); + } + + private void ExecuteSqlTransaction(params string[] sqlStatements) + { + using var transaction = Database.BeginTransaction(); + try + { + foreach (var sql in sqlStatements) + { + Database.ExecuteSqlRaw(sql); + } + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + private async Task ExecuteSqlAsync(string sql) + { + await Database.ExecuteSqlRawAsync(sql); + } + + private async Task ExecuteSqlTransactionAsync(params string[] sqlStatements) + { + using var transaction = await Database.BeginTransactionAsync(); + try + { + foreach (var sql in sqlStatements) + { + await Database.ExecuteSqlRawAsync(sql); + } + await transaction.CommitAsync(); + } + catch + { + await transaction.RollbackAsync(); + throw; + } + } } \ No newline at end of file diff --git a/src/PlayerManagement/TailgrabPanel.xaml.cs b/src/PlayerManagement/TailgrabPanel.xaml.cs index d4f39aa..bd3ab77 100644 --- a/src/PlayerManagement/TailgrabPanel.xaml.cs +++ b/src/PlayerManagement/TailgrabPanel.xaml.cs @@ -146,6 +146,9 @@ public TailgrabPanel(ServiceRegistry serviceRegistry) InitializeComponent(); DataContext = this; + // Set window title with version + Title = $"Tailgrab {BuildInfo.GetInformationalVersion()}"; + ActiveView = CollectionViewSource.GetDefaultView(ActivePlayers); ActiveView.SortDescriptions.Add(new SortDescription("InstanceStartTime", ListSortDirection.Descending)); UpdateHeaderSortIndicator(ActivePlayerInstanceStart, ActiveView, "InstanceStartTime"); diff --git a/tailgrab.csproj b/tailgrab.csproj index 593f0fc..6a3fa1b 100644 --- a/tailgrab.csproj +++ b/tailgrab.csproj @@ -7,10 +7,16 @@ true true enable - 1.1.0.1722 - 1.1.0.1722 - 1.1.0.1722 - src\Resources\tailgrab_large.ico + src\Resources\tailgrab_large.ico + + + $(MSBuildProjectDirectory)\BuildNumber.txt + $([System.IO.File]::ReadAllText('$(BuildNumberFile)').Trim()) + + + 1.1.0.$(BuildNumber) + 1.1.0.$(BuildNumber) + 1.1.0.$(BuildNumber) From f00fb9c7bba3bcd18433a6d300af577d7d69daf6 Mon Sep 17 00:00:00 2001 From: Jay Long Date: Wed, 18 Feb 2026 05:13:28 -0600 Subject: [PATCH 04/70] Fixing autoincremented Build Numbering system --- BuildNumber.txt | 2 +- tailgrab.csproj | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/BuildNumber.txt b/BuildNumber.txt index a85618d..9b5f360 100644 --- a/BuildNumber.txt +++ b/BuildNumber.txt @@ -1 +1 @@ -1722 \ No newline at end of file +1723 diff --git a/tailgrab.csproj b/tailgrab.csproj index 6a3fa1b..824ab17 100644 --- a/tailgrab.csproj +++ b/tailgrab.csproj @@ -117,4 +117,21 @@ + + + + $([System.IO.File]::ReadAllText('$(BuildNumberFile)').Trim()) + $([MSBuild]::Add($(CurrentBuildNumber), 1)) + + + + + + $(NewBuildNumber) + 1.1.0.$(BuildNumber) + 1.1.0.$(BuildNumber) + 1.1.0.$(BuildNumber) + + + \ No newline at end of file From 0240729901659c83a5574120fa952d97ae427fd9 Mon Sep 17 00:00:00 2001 From: Jay Long Date: Wed, 18 Feb 2026 09:56:32 -0600 Subject: [PATCH 05/70] Configuration and Tab setup for multi alert configuration. --- BuildNumber.txt | 2 +- src/Clients/VRChat/VRChat.cs | 14 +- src/Common/CommonConst.cs | 25 ++++ src/Common/ConfigStore.cs | 4 +- src/Common/SoundManager.cs | 40 +++--- src/PlayerManagement/TailgrabPanel.xaml | 156 +++++++++++++++++++-- src/PlayerManagement/TailgrabPanel.xaml.cs | 74 +++++++--- src/Program.cs | 44 +++++- 8 files changed, 298 insertions(+), 61 deletions(-) diff --git a/BuildNumber.txt b/BuildNumber.txt index 9b5f360..c439d95 100644 --- a/BuildNumber.txt +++ b/BuildNumber.txt @@ -1 +1 @@ -1723 +1747 diff --git a/src/Clients/VRChat/VRChat.cs b/src/Clients/VRChat/VRChat.cs index c5377d6..a1acf56 100644 --- a/src/Clients/VRChat/VRChat.cs +++ b/src/Clients/VRChat/VRChat.cs @@ -27,7 +27,11 @@ public async Task Initialize() string? password = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_Password); string? twoFactorSecret = ConfigStore.LoadSecret(Tailgrab.Common.CommonConst.Registry_VRChat_Web_2FactorKey); - if (username is null || password is null || twoFactorSecret is null) + // Persist cookies to disk (cookies.json) for reuse + try + { + + if (username is null || password is null || twoFactorSecret is null) { System.Windows.MessageBox.Show("VR Chat Web API Credentials are not set yet, use the Config / Secrets tab to update credenials and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); return; @@ -94,18 +98,16 @@ public async Task Initialize() } var currentUser = await _vrchat.Authentication.GetCurrentUserAsync(); - Console.WriteLine($"Logged in as \"{currentUser.DisplayName}\""); + logger.Info($"Logged in as \"{currentUser.DisplayName}\""); var cookies = _vrchat.GetCookies(); - // Persist cookies to disk (cookies.json) for reuse - try - { SaveCookiesToFile(cookiePath, cookies); } catch (Exception ex) { - Console.WriteLine($"Failed to save cookies to '{cookiePath}': {ex.Message}"); + logger.Error($"Failed to Log Into VRC and to save cookies': {ex.Message}"); + System.Windows.MessageBox.Show($"Failed to Log Into VRChat Web API, check logs for details. Error: {ex.Message}", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); } } diff --git a/src/Common/CommonConst.cs b/src/Common/CommonConst.cs index db4a505..5134db1 100644 --- a/src/Common/CommonConst.cs +++ b/src/Common/CommonConst.cs @@ -34,5 +34,30 @@ public static class CommonConst // Avatar Gist related registry keys public const string Registry_Avatar_Checksum = "GIST_AVATAR_LIST_CHECKSUM"; public const string Registry_Avatar_Gist = "GIST_AVATAR_LIST_URL"; + + + public const string Registry_AvatarAlertWarn_Sound = "\\Alerts\\AVATAR\\WARN_SOUND"; + public const string Registry_AvatarAlertNuisence_Sound = "\\Alerts\\AVATAR\\NUISENCE_SOUND"; + public const string Registry_AvatarAlertCrasher_Sound = "\\Alerts\\AVATAR\\CRASHER_SOUND"; + + public const string Registry_GroupAlertWarn_Sound = "\\Alerts\\GROUP\\WARN_SOUND"; + public const string Registry_GroupAlertNuisence_Sound = "\\Alerts\\GROUP\\NUISENCE_SOUND"; + public const string Registry_GroupAlertCrasher_Sound = "\\Alerts\\GROUP\\CRASHER_SOUND"; + + public const string Registry_ProfileAlertWarn_Sound = "\\Alerts\\PROFILE\\WARN_SOUND"; + public const string Registry_ProfileAlertNuisence_Sound = "\\Alerts\\PROFILE\\NUISENCE_SOUND"; + public const string Registry_ProfileAlertCrasher_Sound = "\\Alerts\\PROFILE\\CRASHER_SOUND"; + + public const string Registry_AvatarAlertWarn_Color = "\\Alerts\\AVATAR\\WARN_COLOR"; + public const string Registry_AvatarAlertNuisence_Color = "\\Alerts\\AVATAR\\NUISENCE_COLOR"; + public const string Registry_AvatarAlertCrasher_Color = "\\Alerts\\AVATAR\\CRASHER_COLOR"; + + public const string Registry_GroupAlertWarn_Color = "\\Alerts\\GROUP\\WARN_COLOR"; + public const string Registry_GroupAlertNuisence_Color = "\\Alerts\\GROUP\\NUISENCE_COLOR"; + public const string Registry_GroupAlertCrasher_Color = "\\Alerts\\GROUP\\CRASHER_COLOR"; + + public const string Registry_ProfileAlertWarn_v = "\\Alerts\\PROFILE\\WARN_COLOR"; + public const string Registry_ProfileAlertNuisence_Color = "\\Alerts\\PROFILE\\NUISENCE_COLOR"; + public const string Registry_ProfileAlertCrasher_Color = "\\Alerts\\PROFILE\\CRASHER_COLOR"; } } diff --git a/src/Common/ConfigStore.cs b/src/Common/ConfigStore.cs index b929ed0..f4f04d1 100644 --- a/src/Common/ConfigStore.cs +++ b/src/Common/ConfigStore.cs @@ -55,7 +55,7 @@ public static void DeleteSecret(string name) } } - public static string? GetStoredUri(string keyName) + public static string? GetStoredKeyString(string keyName) { try { @@ -84,7 +84,7 @@ public static void DeleteSecret(string name) } } - public static void PutStoredUri(string keyName, string keyValue) + public static void PutStoredKeyString(string keyName, string keyValue) { try { diff --git a/src/Common/SoundManager.cs b/src/Common/SoundManager.cs index e0ffc3a..e7d9877 100644 --- a/src/Common/SoundManager.cs +++ b/src/Common/SoundManager.cs @@ -16,7 +16,7 @@ namespace Tailgrab.Common public static class SoundManager { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - private static readonly string[] allSystemSounds = { "Asterisk", "Beep", "Exclamation", "Warning", "Hand", "Error", "Question" }; + private static readonly string[] allSystemSounds = { "*NONE", "Asterisk", "Beep", "Exclamation", "Warning", "Hand", "Error", "Question" }; /// /// Enumerate available sound base filenames (without extension) from the ./sounds directory. @@ -28,32 +28,32 @@ public static List GetAvailableSounds() { var baseDir = AppContext.BaseDirectory ?? Directory.GetCurrentDirectory(); var soundsDir = Path.Combine(baseDir, "sounds"); - if (!Directory.Exists(soundsDir)) + if (Directory.Exists(soundsDir)) { - return new List(); - } - var exts = new[] { ".wav", ".mp3", ".ogg" }; - var files = Directory.EnumerateFiles(soundsDir) - .Where(f => exts.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase)) - .Select(f => Path.GetFileNameWithoutExtension(f)) - .Where(n => !string.IsNullOrWhiteSpace(n)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(n => n, StringComparer.OrdinalIgnoreCase) - .ToList(); - - files = allSystemSounds - .Concat(files) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - - return files; + var exts = new[] { ".wav", ".mp3", ".ogg" }; + var files = Directory.EnumerateFiles(soundsDir) + .Where(f => exts.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase)) + .Select(f => Path.GetFileNameWithoutExtension(f)) + .Where(n => !string.IsNullOrWhiteSpace(n)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(n => n, StringComparer.OrdinalIgnoreCase) + .ToList(); + + files = allSystemSounds + .Concat(files) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + return files; + } } catch (Exception ex) { Logger.Warn(ex, "Failed to enumerate sounds directory"); - return new List(); } + + return new List(); } public static void PlayAlertSound(string entityType, AlertTypeEnum alertType ) diff --git a/src/PlayerManagement/TailgrabPanel.xaml b/src/PlayerManagement/TailgrabPanel.xaml index 5251075..40580ae 100644 --- a/src/PlayerManagement/TailgrabPanel.xaml +++ b/src/PlayerManagement/TailgrabPanel.xaml @@ -20,8 +20,11 @@ Title="Tailgrab Player Panel" - - + + + + + @@ -29,14 +32,14 @@ Title="Tailgrab Player Panel" - - + + static List OpenedFiles = new List { }; + static Dictionary WatchedFiles = new Dictionary(); /// /// The path to the user's profile directory. @@ -53,22 +54,35 @@ public static async Task TailFileAsync(string filePath) { return; } - OpenedFiles.Add(filePath); - logger.Info($"Tailing file: {filePath}. Press Ctrl+C to stop."); + logger.Info($"Tailing file: {filePath}"); using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (StreamReader sr = new StreamReader(fs, Encoding.UTF8)) { + // Start at the end of the file long lastMaxOffset = fs.Length; fs.Seek(lastMaxOffset, SeekOrigin.Begin); + OpenedFiles.Add(filePath); + WatchedFiles[filePath] = new FileWatchItem(lastMaxOffset); + while (true) { // If the file size hasn't changed, wait if (fs.Length == lastMaxOffset) { + if (WatchedFiles.ContainsKey(filePath)) + { + WatchedFiles[filePath].ElapsedTime += 1; + if (WatchedFiles[filePath].ElapsedTime >= 9000) // If we've been watching this file for 15 minutes without changes + { + logger.Info($"Timeout waiting for new lines in '{filePath}'"); + return; + } + } + await Task.Delay(100); // Adjust delay as needed continue; } @@ -88,6 +102,9 @@ public static async Task TailFileAsync(string filePath) // Update the offset to the new end of the file lastMaxOffset = fs.Length; + + // Reset the watch counter for this file since we have new data + WatchedFiles.Remove(filePath); } } } @@ -116,10 +133,12 @@ public static async Task WatchPath(string path) // ensure existing files are tailed immediately try { - string _todaysFile = $"output_log_{DateTime.Now:yyyy_MM_dd}*.txt"; + string _todaysFile = $"output_log_{DateTime.Now:yyyy-MM-dd}_*.txt"; + logger.Debug($"Looking for existing log files matching: {_todaysFile}"); var existing = Directory.GetFiles(LogWatcher.Path, _todaysFile); foreach (var f in existing) { + logger.Debug($"Found File to Tail: {f}"); _ = Task.Run(() => TailFileAsync(f)); } } @@ -655,8 +674,8 @@ private static void BuildAppWindow(ServiceRegistry serviceRegistryInstance) var lightText = new SolidColorBrush(System.Windows.Media.Color.FromRgb(230, 230, 230)); var accent = new SolidColorBrush(System.Windows.Media.Color.FromRgb(29, 44, 55)); - var highlightDark = new SolidColorBrush(System.Windows.Media.Color.FromRgb(112, 112, 174)); - var highlightDarkText = new SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 0, 0)); + var highlightDark = new SolidColorBrush(System.Windows.Media.Color.FromRgb(70, 70, 109)); + var highlightDarkText = new SolidColorBrush(System.Windows.Media.Color.FromRgb(200, 200, 129)); // Override common system brushes @@ -914,4 +933,15 @@ public static string GetInformationalVersion() ?.InformationalVersion ?? GetAssemblyVersion(); } +} + +public class FileWatchItem +{ + public long StartingSize { get; set; } + public int ElapsedTime { get; set; } + public FileWatchItem(long startingSize) + { + StartingSize = startingSize; + ElapsedTime = 0; + } } \ No newline at end of file From ee96f9f87e24f7018e098dceb19bb7357e459484 Mon Sep 17 00:00:00 2001 From: Jay Long Date: Sun, 22 Feb 2026 15:29:26 -0600 Subject: [PATCH 19/70] Adding Avatar Uploader Name to the Entity --- src/AvatarManagement/AvatarManagement.cs | 13 +++++++++---- src/Models/AvatarInfo.cs | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/AvatarManagement/AvatarManagement.cs b/src/AvatarManagement/AvatarManagement.cs index fa73ab1..b432e63 100644 --- a/src/AvatarManagement/AvatarManagement.cs +++ b/src/AvatarManagement/AvatarManagement.cs @@ -87,7 +87,6 @@ private void EnqueueAvatarForCheck(string avatarId) { if ((DateTime.UtcNow - dateTime).TotalMinutes < 60) { - logger.Debug($"Skipping adding avatar {avatarId} as it was recently processed."); return; } } @@ -130,7 +129,8 @@ public void GetAvatarsFromUser(string userId, string avatarName) CreatedAt = avatar.CreatedAt, UpdatedAt = DateTime.UtcNow, IsBos = false, - AlertType = AlertTypeEnum.None + AlertType = AlertTypeEnum.None, + UserName = avatar.AuthorName }; AddAvatar(avatarInfo); @@ -138,6 +138,7 @@ public void GetAvatarsFromUser(string userId, string avatarName) else { dbAvatarInfo.UserId = avatar.AuthorId; + dbAvatarInfo.UserName = avatar.AuthorName; dbAvatarInfo.AvatarName = avatar.Name; dbAvatarInfo.ImageUrl = avatar.ImageUrl; dbAvatarInfo.CreatedAt = avatar.CreatedAt; @@ -196,18 +197,20 @@ public static async Task AvatarCheckTask(ConcurrentPriorityQueue= DateTime.UtcNow.AddHours(-24))) + else if (dbAvatarInfo.AlertType == AlertTypeEnum.None && + (!dbAvatarInfo.UpdatedAt.HasValue || dbAvatarInfo.UpdatedAt.Value >= DateTime.UtcNow.AddHours(-2))) { updateNeeded = true; } if (updateNeeded) { + // Adds and Updates avatar info in the database, if it doesn't exist or was last updated more than 2 hours ago Avatar? avatarData = FetchUpdateAvatarData(serviceRegistry, dBContext, item.AvatarId, dbAvatarInfo); if (avatarData == null && dbAvatarInfo == null) { + // Private Avatar CreateAvatarInfoForPrivate(dBContext, item.AvatarId); } @@ -272,6 +275,7 @@ private static void CreateAvatarInfoForPrivate(TailgrabDBContext dBContext, stri { AvatarId = avatarData.Id, UserId = avatarData.AuthorId, + UserName = avatarData.Name, AvatarName = avatarData.Name, ImageUrl = avatarData.ImageUrl, CreatedAt = avatarData.CreatedAt, @@ -300,6 +304,7 @@ private static void CreateAvatarInfoForPrivate(TailgrabDBContext dBContext, stri } dbAvatarInfo.UserId = avatarData.AuthorId; + dbAvatarInfo.UserName = avatarData.AuthorName; dbAvatarInfo.AvatarName = avatarData.Name; dbAvatarInfo.ImageUrl = avatarData.ImageUrl; dbAvatarInfo.CreatedAt = avatarData.CreatedAt; diff --git a/src/Models/AvatarInfo.cs b/src/Models/AvatarInfo.cs index e7e5f39..fd84095 100644 --- a/src/Models/AvatarInfo.cs +++ b/src/Models/AvatarInfo.cs @@ -24,6 +24,8 @@ public partial class AvatarInfo public string ImageUrl { get; set; } + public string UserName { get; set; } + public AvatarInfo() { CreatedAt = DateTime.UtcNow; @@ -31,6 +33,6 @@ public AvatarInfo() public override string ToString() { - return $"GroupId: {AvatarId}, UserId: {UserId}, GroupName: {AvatarName}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}, IsBOS: {IsBos}, AlertType: {AlertType}"; + return $"AvatarInfo: AvatarId={AvatarId}, UserId={UserId}, AvatarName={AvatarName}, CreatedAt={CreatedAt}, UpdatedAt={UpdatedAt}, IsBos={IsBos}, AlertType={AlertType}, ImageUrl={ImageUrl}, UserName={UserName}"; } } \ No newline at end of file From 781990d52784c7c14715d152080856d21cfeb6de Mon Sep 17 00:00:00 2001 From: Jay Long Date: Sun, 22 Feb 2026 15:30:13 -0600 Subject: [PATCH 20/70] Updating the UI for Highlights & Past Player historical reporting. --- BuildNumber.txt | 2 +- src/PlayerManagement/PlayerManagement.cs | 12 +- src/PlayerManagement/TailgrabPanel.xaml | 201 +++++++-- src/PlayerManagement/TailgrabPanel.xaml.cs | 461 +++++++++++++++------ 4 files changed, 505 insertions(+), 171 deletions(-) diff --git a/BuildNumber.txt b/BuildNumber.txt index bbd4bf1..00d8cfa 100644 --- a/BuildNumber.txt +++ b/BuildNumber.txt @@ -1 +1 @@ -1774 +1802 diff --git a/src/PlayerManagement/PlayerManagement.cs b/src/PlayerManagement/PlayerManagement.cs index 6dab7a8..55cd2fa 100644 --- a/src/PlayerManagement/PlayerManagement.cs +++ b/src/PlayerManagement/PlayerManagement.cs @@ -1,8 +1,5 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.VisualBasic.ApplicationServices; using NLog; using System.Text; -using System.Windows; using Tailgrab.Clients.VRChat; using Tailgrab.Common; using Tailgrab.LineHandler; @@ -69,7 +66,7 @@ public class PlayerPrint public string AIClass { get; set; } public string AuthorName { get; set; } - public PlayerPrint(VRChat.API.Model.Print p, string aiEvaluation, string aIClass) + public PlayerPrint(VRChat.API.Model.Print p, string aiEvaluation, string aiClassification) { PrintId = p.Id; OwnerId = p.OwnerId; @@ -78,7 +75,7 @@ public PlayerPrint(VRChat.API.Model.Print p, string aiEvaluation, string aIClass PrintUrl = p.Files.Image; AuthorName = p.AuthorName; AIEvaluation = aiEvaluation; - AIClass = aIClass; + AIClass = aiClassification; } } @@ -644,7 +641,7 @@ internal async void AddInventorySpawn(string userId, string inventoryId) if (!aiEvaluation.Equals("OK")) { AddPlayerEventByUserId(userId, PlayerEvent.EventType.Emoji, $"AI Evaluation: Spawned Item {itemName} ({inventoryId}) was classified {evaluated}"); - player.AddAlertMessage(AlertClassEnum.EmojiSticker, AlertTypeEnum.Nuisance, "Yellow", $"{evaluated}"); + player.AddAlertMessage(AlertClassEnum.EmojiSticker, AlertTypeEnum.Nuisance, "Yellow", $"{aiEvaluation}"); } } } @@ -741,8 +738,7 @@ internal async void AddPrintData(string printId) } } player?.PrintData[printId] = new PlayerPrint(printInfo, evaluated ?? "Not Evaluated", aiEvaluation); - } - + } } } } diff --git a/src/PlayerManagement/TailgrabPanel.xaml b/src/PlayerManagement/TailgrabPanel.xaml index 26c7c48..3c4e28b 100644 --- a/src/PlayerManagement/TailgrabPanel.xaml +++ b/src/PlayerManagement/TailgrabPanel.xaml @@ -19,13 +19,6 @@ Title="Tailgrab Player Panel" - - - - - - - @@ -33,16 +26,20 @@ Title="Tailgrab Player Panel" - +