diff --git a/Settings/Base/BaseSettings.cs b/Settings/Base/BaseSettings.cs new file mode 100644 index 0000000..793d96b --- /dev/null +++ b/Settings/Base/BaseSettings.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Windows.Media; +using System.Windows.Threading; +using ff14bot.Helpers; +using LlamaLibrary.Logging; +using LlamaLibrary.Memory; +using Newtonsoft.Json; +using LogLevel = LlamaLibrary.Logging.LogLevel; + +namespace LlamaLibrary.Settings.Base; + +using LogLevel = LogLevel; + +public abstract class BaseSettings : INotifyPropertyChanged +{ + private readonly LLogger _logger; + private readonly DebounceDispatcher _saveDebounceDispatcher; + private Dictionary? _debounceDispatchers; + private bool _loaded; + + public BaseSettings(string path) + { + Dispatcher = Dispatcher.CurrentDispatcher; + _saveDebounceDispatcher = new DebounceDispatcher(SaveLocal); + _logger = new LLogger($"{GetType().Name}", Colors.Peru, LogLevel.Debug); + FilePath = path; + LoadFrom(FilePath); + } + + public static string AssemblyDirectory => JsonSettings.AssemblyPath ?? throw new InvalidOperationException(); + + public static string AssemblyPath => AssemblyDirectory; + + public static string SettingsPath => Path.Combine(AssemblyPath, "Settings"); + + private Dispatcher Dispatcher { get; } + + [JsonIgnore] + private string FilePath { get; } + + public static string GetSettingsFilePath(params string[] subPathParts) + { + var list = new List { SettingsPath }; + list.AddRange(subPathParts); + return Path.Combine(list.ToArray()); + } + + protected void LoadFrom(string file) + { + var properties = GetType().GetProperties(); + + foreach (var propertyInfo in properties) + { + _debounceDispatchers ??= new Dictionary(); + _debounceDispatchers[propertyInfo.Name] = new DebounceDispatcher(OnPropertyChangedDebounce); + + var customAttributes = propertyInfo.GetCustomAttributes(true).ToList(); + + if (customAttributes.Count == 0) + { + continue; + } + + foreach (var custom in customAttributes) + { + if (propertyInfo.GetSetMethod() != null) + { + propertyInfo.SetValue(this, custom.Value, null); + } + } + } + + if (File.Exists(file)) + { + try + { + Dispatcher.Invoke(() => + { + try + { + JsonConvert.PopulateObject(File.ReadAllText(file), this); + } + catch (Exception e) + { + _logger.Exception(e); + } + }); + + foreach (var propertyInfo in properties) + { + //Check if property is a observable collection + if (typeof(INotifyCollectionChanged).IsAssignableFrom(propertyInfo.PropertyType)) + { + //Set list changed event to trigger on property change + var collection = propertyInfo.GetValue(this) as INotifyCollectionChanged; + if (collection != null) + { + if (propertyInfo.PropertyType.IsGenericType && typeof(INotifyPropertyChanged).IsAssignableFrom(propertyInfo.PropertyType.GenericTypeArguments[0])) + { + collection.CollectionChanged += (_, args) => + { + OnPropertyChanged(propertyInfo.Name); + + if (args is { Action: NotifyCollectionChangedAction.Add, NewItems: not null }) + { + foreach (var item in args.NewItems) + { + if (item is INotifyPropertyChanged notifyPropertyChanged) + { + notifyPropertyChanged.PropertyChanged += OnNotifyPropertyChangedOnPropertyChanged; + } + } + } + + if (args is { Action: NotifyCollectionChangedAction.Remove, OldItems: not null }) + { + foreach (var item in args.OldItems) + { + if (item is INotifyPropertyChanged notifyPropertyChanged) + { + notifyPropertyChanged.PropertyChanged -= OnNotifyPropertyChangedOnPropertyChanged; + } + } + } + + return; + + void OnNotifyPropertyChangedOnPropertyChanged(object? o, PropertyChangedEventArgs eventArgs) + { + OnPropertyChanged(propertyInfo.Name); + } + }; + } + else + { + collection.CollectionChanged += (_, _) => { OnPropertyChanged(propertyInfo.Name); }; + } + } + else + { + _logger.Error($"Property {propertyInfo.Name} is not an INotifyCollectionChanged it is {propertyInfo.PropertyType}"); + } + } + + if (typeof(IBindingList).IsAssignableFrom(propertyInfo.PropertyType)) + { + //Set list changed event to trigger on property change + var collection = propertyInfo.GetValue(this) as IBindingList; + if (collection != null) + { + collection.ListChanged += (_, _) => { OnPropertyChanged(propertyInfo.Name); }; + } + else + { + _logger.Error($"Property {propertyInfo.Name} is not an IBindingList it is {propertyInfo.PropertyType}"); + } + } + + if (typeof(INotifyPropertyChanged).IsAssignableFrom(propertyInfo.PropertyType)) + { + var notifyPropertyChanged = propertyInfo.GetValue(this) as INotifyPropertyChanged; + if (notifyPropertyChanged != null) + { + notifyPropertyChanged.PropertyChanged += (_, _) => { OnPropertyChanged(propertyInfo.Name); }; + } + else + { + _logger.Error($"Property {propertyInfo.Name} is not an INotifyPropertyChanged it is {propertyInfo.PropertyType}"); + } + } + } + } + catch (Exception e) + { + _logger.Exception(e); + } + } + + _loaded = true; + if (file != FilePath || !File.Exists(file)) + { + Save(); + } + } + + public virtual void Save() + { + _saveDebounceDispatcher.Debounce(500, null!, DispatcherPriority.ApplicationIdle, Dispatcher); + } + + private void SaveLocal(object? state = null) + { + SaveAs(FilePath); + } + + public void SaveAs(string file) + { + try + { + if (!_loaded) + { + return; + } + + if (!File.Exists(file)) + { + var directoryName = Path.GetDirectoryName(file); + if (directoryName != null && !Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + } + + File.WriteAllText(file, JsonConvert.SerializeObject(this, Formatting.Indented)); + } + catch (Exception e) + { + _logger.Exception(e); + } + } + + private void OnPropertyChangedDebounce(object propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs((string)propertyName)); + Save(); + } + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + if (propertyName == null) + { + return; + } + + _debounceDispatchers![propertyName].Debounce(50, propertyName, DispatcherPriority.ApplicationIdle, Dispatcher); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + field = value; + OnPropertyChanged(propertyName); + return true; + } + + public event PropertyChangedEventHandler? PropertyChanged; +} \ No newline at end of file diff --git a/Settings/Base/BaseSettingsTyped.cs b/Settings/Base/BaseSettingsTyped.cs new file mode 100644 index 0000000..f70f306 --- /dev/null +++ b/Settings/Base/BaseSettingsTyped.cs @@ -0,0 +1,73 @@ +using ff14bot; +using LlamaLibrary.Extensions; + +namespace LlamaLibrary.Settings.Base; + +public class BaseSettings : BaseSettings + where T : BaseSettings, new() +{ + private static T? _instance; + + public BaseSettings() : base(GetSettingsFilePath($"{typeof(T).Name}.json")) + { + } + + public BaseSettings(string settingsFilePath) : base(settingsFilePath) + { + } + + public static T Instance + { + get => _instance ??= new T(); + set => _instance = value; + } + + public static void SetInstance(T instance) + { + _instance = instance; + } +} + +public class CharacterBaseSettings : BaseSettings + where T : BaseSettings, new() +{ + public CharacterBaseSettings() : base(GetSettingsFilePath($"{Core.Me.Name}_{Core.Me.PlayerId()}", $"{typeof(T).Name}.json")) + { + } + + public CharacterBaseSettings(string fileName) : base(GetSettingsFilePath($"{Core.Me.Name}_{Core.Me.PlayerId()}", fileName)) + { + } +} + +public class AccountBaseSettings : BaseSettings + where T : BaseSettings, new() +{ + public AccountBaseSettings() : base(GetSettingsFilePath($"Account_{Core.Me.AccountId()}", $"{typeof(T).Name}.json")) + { + } + + public AccountBaseSettings(string fileName) : base(GetSettingsFilePath($"Account_{Core.Me.AccountId()}", fileName)) + { + } + + public AccountBaseSettings(int accountId, string fileName) : base(GetSettingsFilePath($"Account_{accountId}", fileName)) + { + } + + public AccountBaseSettings(int accountId) : base(GetSettingsFilePath($"Account_{accountId}", $"{typeof(T).Name}.json")) + { + } +} + +public class HomeWorldBaseSettings : BaseSettings + where T : BaseSettings, new() +{ + public HomeWorldBaseSettings() : base(GetSettingsFilePath($"HomeWorld_{Core.Me.HomeWorld()}", $"{typeof(T).Name}.json")) + { + } + + public HomeWorldBaseSettings(string fileName) : base(GetSettingsFilePath($"HomeWorld_{Core.Me.HomeWorld()}", fileName)) + { + } +} \ No newline at end of file