From f3b8a9b0a5bdaefd16ddd53c1899e160e23dc36a Mon Sep 17 00:00:00 2001 From: iLollek Date: Mon, 23 Dec 2024 20:32:07 +0100 Subject: [PATCH 1/2] feat: Change time (#58) & Begin Attributes (#61) --- Aerochat/Attributes/DisplayAttribute.cs | 24 ++++++++ Aerochat/Converter/EnumToStringConverter.cs | 53 +++++++++++++++++ Aerochat/Converter/TimeFormatConverter.cs | 33 +++++++++++ Aerochat/Enums/TimeFormat.cs | 18 ++++++ Aerochat/Helpers/EnumHelper.cs | 39 +++++++++++++ Aerochat/Settings/SettingsManager.cs | 6 +- Aerochat/ViewModels/Base.cs | 5 ++ Aerochat/ViewModels/Message.cs | 48 +++++++++++++--- Aerochat/ViewModels/Settings.cs | 20 ++++++- Aerochat/Windows/Chat.xaml | 9 ++- Aerochat/Windows/Chat.xaml.cs | 32 +++++++++++ Aerochat/Windows/Settings.xaml | 27 +++++++++ Aerochat/Windows/Settings.xaml.cs | 64 +++++++++++++++++++-- 13 files changed, 362 insertions(+), 16 deletions(-) create mode 100644 Aerochat/Attributes/DisplayAttribute.cs create mode 100644 Aerochat/Converter/EnumToStringConverter.cs create mode 100644 Aerochat/Converter/TimeFormatConverter.cs create mode 100644 Aerochat/Enums/TimeFormat.cs create mode 100644 Aerochat/Helpers/EnumHelper.cs diff --git a/Aerochat/Attributes/DisplayAttribute.cs b/Aerochat/Attributes/DisplayAttribute.cs new file mode 100644 index 00000000..15773cf9 --- /dev/null +++ b/Aerochat/Attributes/DisplayAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +// (iL - 21.12.2024) This is basically another abstraction layer for developers. +// Let's say you have a beautiful DropDown for the Settings, but your Enum Variable Names are pretty cryptic +// for the average End-User. With Aerochat.Attributes, you can show the user a beautiful string in the UI, +// while here in the Code you can keep your cryptic and scary sounding Names. + +namespace Aerochat.Attributes +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class DisplayAttribute : Attribute + { + public string Name { get; set; } + + public DisplayAttribute(string name) + { + Name = name; + } + } +} diff --git a/Aerochat/Converter/EnumToStringConverter.cs b/Aerochat/Converter/EnumToStringConverter.cs new file mode 100644 index 00000000..8d2cd493 --- /dev/null +++ b/Aerochat/Converter/EnumToStringConverter.cs @@ -0,0 +1,53 @@ +using Aerochat.Attributes; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Aerochat.Windows +{ + public class EnumToStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + return string.Empty; // Default empty string for no selection + + if (value is Enum enumValue) + { + var fieldInfo = enumValue.GetType().GetField(enumValue.ToString()); + var displayAttribute = fieldInfo?.GetCustomAttribute(); + return displayAttribute?.Name ?? enumValue.ToString(); + } + + return value.ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + // Handle conversion back from the display name to enum + if (value == null || string.IsNullOrEmpty(value.ToString())) + return null; + + try + { + var enumType = targetType; + var enumValue = Enum.GetValues(enumType) + .Cast() + .FirstOrDefault(e => e.GetType() + .GetField(e.ToString()) + ?.GetCustomAttribute()?.Name == value.ToString()); + + return enumValue; + } + catch + { + return null; + } + } + } +} diff --git a/Aerochat/Converter/TimeFormatConverter.cs b/Aerochat/Converter/TimeFormatConverter.cs new file mode 100644 index 00000000..16fb1a82 --- /dev/null +++ b/Aerochat/Converter/TimeFormatConverter.cs @@ -0,0 +1,33 @@ +using Aerochat.Enums; +using Aerochat.Settings; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Aerochat.Windows +{ + public class TimeFormatConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is DateTime dateTime) + { + // Retrieve SelectedTimeFormat from the SettingsManager + var format = SettingsManager.Instance.SelectedTimeFormat; + string formatString = format == TimeFormat.TwentyFourHour ? "HH:mm:ss" : "h:mm tt"; + + return dateTime.ToString(formatString); + } + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Aerochat/Enums/TimeFormat.cs b/Aerochat/Enums/TimeFormat.cs new file mode 100644 index 00000000..982ae209 --- /dev/null +++ b/Aerochat/Enums/TimeFormat.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Aerochat.Attributes; + +namespace Aerochat.Enums +{ + public enum TimeFormat + { + [Display("24-Hour Clock")] + TwentyFourHour, + + [Display("12-Hour Clock (AM/PM)")] + TwelveHour + } +} diff --git a/Aerochat/Helpers/EnumHelper.cs b/Aerochat/Helpers/EnumHelper.cs new file mode 100644 index 00000000..ba18d828 --- /dev/null +++ b/Aerochat/Helpers/EnumHelper.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Aerochat.Attributes; + +namespace Aerochat.Helpers +{ + public static class EnumHelper + { + public static string GetDisplayName(Enum enumValue) + { + var fieldInfo = enumValue.GetType() + .GetField(enumValue.ToString(), BindingFlags.Public | BindingFlags.Static); + + if (fieldInfo != null) + { + var displayAttribute = fieldInfo.GetCustomAttribute(); + + if (displayAttribute != null) + { + return displayAttribute.Name; + } + } + + return enumValue.ToString(); + } + + public static List<(string DisplayName, T EnumValue)> GetEnumDisplayList() where T : Enum + { + return Enum.GetValues(typeof(T)) + .Cast() + .Select(e => (GetDisplayName(e), e)) // Create tuple of DisplayName and EnumValue + .ToList(); + } + } +} diff --git a/Aerochat/Settings/SettingsManager.cs b/Aerochat/Settings/SettingsManager.cs index 915edcec..4ac2942c 100644 --- a/Aerochat/Settings/SettingsManager.cs +++ b/Aerochat/Settings/SettingsManager.cs @@ -1,4 +1,5 @@ -using Aerochat.ViewModels; +using Aerochat.Enums; +using Aerochat.ViewModels; using System; using System.IO; using System.Linq; @@ -103,6 +104,9 @@ public static void Load() [Settings("Appearance", "Show community-submitted ads on the home page")] public bool DisplayAds { get; set; } = true; + + [Settings("Appearance", "Time format")] + public TimeFormat SelectedTimeFormat { get; set; } = TimeFormat.TwentyFourHour; #endregion } } diff --git a/Aerochat/ViewModels/Base.cs b/Aerochat/ViewModels/Base.cs index 159400dc..1c46f514 100644 --- a/Aerochat/ViewModels/Base.cs +++ b/Aerochat/ViewModels/Base.cs @@ -22,6 +22,11 @@ public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) { if (!EqualityComparer.Default.Equals(field, newValue)) diff --git a/Aerochat/ViewModels/Message.cs b/Aerochat/ViewModels/Message.cs index 3d7fcc07..da9f328d 100644 --- a/Aerochat/ViewModels/Message.cs +++ b/Aerochat/ViewModels/Message.cs @@ -1,10 +1,14 @@ -using Aerochat.Hoarder; +using Aerochat.Enums; +using Aerochat.Hoarder; +using Aerochat.Settings; using DSharpPlus; using DSharpPlus.Entities; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -16,7 +20,6 @@ public class MessageViewModel : ViewModelBase private UserViewModel? _author; private string _message; private string _rawMessage; - private DateTime _timestamp; private ulong? _id; private bool _ephemeral = false; private bool _special = false; @@ -43,11 +46,6 @@ public string RawMessage get => _rawMessage; set => SetProperty(ref _rawMessage, value); } - public DateTime Timestamp - { - get => _timestamp; - set => SetProperty(ref _timestamp, value); - } public ulong? Id { get => _id; @@ -94,6 +92,42 @@ public DiscordMessage MessageEntity set => SetProperty(ref _messageEntity, value); } + public string TimestampString + { + get + { + var format = SettingsManager.Instance.SelectedTimeFormat == TimeFormat.TwentyFourHour ? "HH:mm" : "h:mm tt"; + return Timestamp.ToString(format, CultureInfo.InvariantCulture); // Ensure InvariantCulture to control formatting: Otherwise we will lose the AM/PM at the end! + } + } + + private DateTime _timestamp; + public DateTime Timestamp + { + get { return _timestamp; } + set + { + if (_timestamp != value) + { + _timestamp = value; + RaisePropertyChanged(nameof(Timestamp)); + RaisePropertyChanged(nameof(TimestampString)); + } + } + } + + public virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public event PropertyChangedEventHandler PropertyChanged; + + public void RaisePropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + public ObservableCollection Attachments { get; } = new(); public ObservableCollection Embeds { get; } = new(); diff --git a/Aerochat/ViewModels/Settings.cs b/Aerochat/ViewModels/Settings.cs index 814d329b..c76d504b 100644 --- a/Aerochat/ViewModels/Settings.cs +++ b/Aerochat/ViewModels/Settings.cs @@ -1,9 +1,12 @@ -using System; +using Aerochat.Enums; +using Aerochat.Helpers; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; +using static Vanara.PInvoke.ShlwApi; namespace Aerochat.ViewModels { @@ -13,6 +16,10 @@ public class SettingViewModel : ViewModelBase private string _name; private string _defaultValue; + public ObservableCollection EnumValues { get; set; } = new ObservableCollection(); + public ObservableCollection TimeFormatOptions { get; set; } + public TimeFormat SelectedTimeFormat { get; set; } + public string Type { get => _type; @@ -28,6 +35,17 @@ public string DefaultValue get => _defaultValue; set => SetProperty(ref _defaultValue, value); } + + private string _selectedEnumValue; + public string SelectedEnumValue + { + get => _selectedEnumValue; + set + { + _selectedEnumValue = value; + OnPropertyChanged(); + } + } } public class SettingsCategory : ViewModelBase diff --git a/Aerochat/Windows/Chat.xaml b/Aerochat/Windows/Chat.xaml index 9608e769..8d749c56 100644 --- a/Aerochat/Windows/Chat.xaml +++ b/Aerochat/Windows/Chat.xaml @@ -50,7 +50,9 @@ - + + + - + + + + diff --git a/Aerochat/Windows/Chat.xaml.cs b/Aerochat/Windows/Chat.xaml.cs index a0a32bc2..b5f3be03 100644 --- a/Aerochat/Windows/Chat.xaml.cs +++ b/Aerochat/Windows/Chat.xaml.cs @@ -38,6 +38,7 @@ using Timer = System.Timers.Timer; using DSharpPlus.Exceptions; using static Aerochat.Windows.ToolbarItem; +using Aerochat.Enums; namespace Aerochat.Windows { @@ -731,6 +732,10 @@ public Chat(ulong id, bool allowDefault = false) }; ViewModel.Messages.CollectionChanged += UpdateHiddenInfo; TypingUsers.CollectionChanged += TypingUsers_CollectionChanged; + + // (iL - 20.12.2024) Subscribe to settings changes for live update + SettingsManager.Instance.PropertyChanged += OnSettingsChanged; + Closing += Chat_Closing; Loaded += Chat_Loaded; Discord.Client.TypingStarted += OnType; @@ -745,6 +750,33 @@ public Chat(ulong id, bool allowDefault = false) DrawingCanvas.Strokes.StrokesChanged += Strokes_StrokesChanged; } + private void OnSettingsChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SettingsManager.Instance.SelectedTimeFormat)) + { + Dispatcher.Invoke(() => + { + foreach (var message in ViewModel.Messages) + { + // Update each message + message.RaisePropertyChanged(nameof(MessageViewModel.TimestampString)); + } + + // Force the collection to refresh + // (iL - 21.12.2024) I know that this is a really shitty way to force the UI to update, + // but I wasn't able to implement the live updating any other way after + // fooling around with it for an hour. + // Maybe you have a better idea? :-) + var tempMessages = ViewModel.Messages.ToList(); + ViewModel.Messages.Clear(); + foreach (var msg in tempMessages) + { + ViewModel.Messages.Add(msg); + } + }); + } + } + private async Task OnVoiceStateUpdated(DiscordClient sender, DSharpPlus.EventArgs.VoiceStateUpdateEventArgs args) { if (args.Guild.Id != Channel.Guild?.Id) return; diff --git a/Aerochat/Windows/Settings.xaml b/Aerochat/Windows/Settings.xaml index a5bf2e6e..b0990bc3 100644 --- a/Aerochat/Windows/Settings.xaml +++ b/Aerochat/Windows/Settings.xaml @@ -113,6 +113,7 @@ + + + + + + diff --git a/Aerochat/Windows/Settings.xaml.cs b/Aerochat/Windows/Settings.xaml.cs index 2eaae7b5..a5eeccf5 100644 --- a/Aerochat/Windows/Settings.xaml.cs +++ b/Aerochat/Windows/Settings.xaml.cs @@ -16,6 +16,8 @@ using System.Windows.Media.Imaging; using System.Windows.Shapes; using Vanara.PInvoke; +using Aerochat.Attributes; +using System.Collections.ObjectModel; namespace Aerochat.Windows { @@ -45,21 +47,51 @@ public Settings() } } + private string GetEnumDisplayName(Enum enumValue) + { + // Get the field corresponding to the enum value + var field = enumValue.GetType().GetField(enumValue.ToString()); + // Get the DisplayAttribute from the field + var attribute = (DisplayAttribute)Attribute.GetCustomAttribute(field, typeof(DisplayAttribute)); + + return attribute?.Name ?? enumValue.ToString(); // Return the display name or the enum value if no display name is found + } + public List GetSettingsFromCategory(string category) { var properties = SettingsManager.Instance.GetType() .GetProperties() .Where(prop => prop.GetCustomAttribute()?.Category == category); + var settings = new List(); + foreach (var prop in properties) { - settings.Add(new SettingViewModel + if (prop.PropertyType.IsEnum) { - Name = prop.GetCustomAttribute()!.DisplayName, - Type = prop.PropertyType.Name, - DefaultValue = prop.GetValue(SettingsManager.Instance).ToString() - }); + // Get the enum values as strings + var enumValues = Enum.GetNames(prop.PropertyType).ToList(); + + settings.Add(new SettingViewModel + { + Name = prop.GetCustomAttribute()!.DisplayName, + Type = "Enum", + DefaultValue = prop.GetValue(SettingsManager.Instance)?.ToString(), + EnumValues = new ObservableCollection(enumValues), + SelectedEnumValue = prop.GetValue(SettingsManager.Instance)?.ToString() + }); + } + else + { + settings.Add(new SettingViewModel + { + Name = prop.GetCustomAttribute()!.DisplayName, + Type = prop.PropertyType.Name, + DefaultValue = prop.GetValue(SettingsManager.Instance)?.ToString() + }); + } } + return settings; } @@ -115,5 +147,27 @@ private void CheckBox_Click(object sender, RoutedEventArgs e) // save the settings SettingsManager.Save(); } + + private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + var comboBox = (ComboBox)sender; + var name = comboBox.Tag.ToString(); + var selectedValue = comboBox.SelectedItem.ToString(); + + // Find the corresponding property in SettingsManager + var property = SettingsManager.Instance.GetType() + .GetProperties() + .FirstOrDefault(prop => prop.GetCustomAttribute()?.DisplayName == name); + + // If it's an Enum, set the value + if (property!.PropertyType.IsEnum) + { + var enumValue = Enum.Parse(property.PropertyType, selectedValue!); + property.SetValue(SettingsManager.Instance, enumValue); + } + + // Save the settings + SettingsManager.Save(); + } } } From 13762bf85ca2921b337e6dbe6b68b405c57afff9 Mon Sep 17 00:00:00 2001 From: iLollek Date: Mon, 23 Dec 2024 20:41:45 +0100 Subject: [PATCH 2/2] feat: Actually Display whats set in DisplayAttribute (#61) --- Aerochat/Windows/Settings.xaml.cs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Aerochat/Windows/Settings.xaml.cs b/Aerochat/Windows/Settings.xaml.cs index a5eeccf5..9caf1338 100644 --- a/Aerochat/Windows/Settings.xaml.cs +++ b/Aerochat/Windows/Settings.xaml.cs @@ -69,16 +69,19 @@ public List GetSettingsFromCategory(string category) { if (prop.PropertyType.IsEnum) { - // Get the enum values as strings - var enumValues = Enum.GetNames(prop.PropertyType).ToList(); + // Get the enum values with their display names + var enumValues = Enum.GetValues(prop.PropertyType) + .Cast() + .Select(e => new KeyValuePair(e.ToString(), GetEnumDisplayName(e))) + .ToList(); settings.Add(new SettingViewModel { Name = prop.GetCustomAttribute()!.DisplayName, Type = "Enum", DefaultValue = prop.GetValue(SettingsManager.Instance)?.ToString(), - EnumValues = new ObservableCollection(enumValues), - SelectedEnumValue = prop.GetValue(SettingsManager.Instance)?.ToString() + EnumValues = new ObservableCollection(enumValues.Select(ev => ev.Value)), + SelectedEnumValue = GetEnumDisplayName((Enum)prop.GetValue(SettingsManager.Instance)) }); } else @@ -159,11 +162,17 @@ private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs .GetProperties() .FirstOrDefault(prop => prop.GetCustomAttribute()?.DisplayName == name); - // If it's an Enum, set the value + // If it's an Enum, map display name to enum value if (property!.PropertyType.IsEnum) { - var enumValue = Enum.Parse(property.PropertyType, selectedValue!); - property.SetValue(SettingsManager.Instance, enumValue); + var enumValue = Enum.GetValues(property.PropertyType) + .Cast() + .FirstOrDefault(e => GetEnumDisplayName(e) == selectedValue); + + if (enumValue != null) + { + property.SetValue(SettingsManager.Instance, enumValue); + } } // Save the settings