Ruben 7 meses atrás
pai
commit
45fd20c82b

+ 58 - 43
src/PicView.Avalonia/Clipboard/ClipboardImageOperations.cs

@@ -11,12 +11,12 @@ using PicView.Core.Localization;
 namespace PicView.Avalonia.Clipboard;
 
 /// <summary>
-/// Handles clipboard operations related to images
+///     Handles clipboard operations related to images
 /// </summary>
 public static class ClipboardImageOperations
 {
     /// <summary>
-    /// Copies the current image to the clipboard
+    ///     Copies the current image to the clipboard
     /// </summary>
     /// <param name="vm">The main view model</param>
     /// <returns>A task representing the asynchronous operation</returns>
@@ -51,7 +51,7 @@ public static class ClipboardImageOperations
     }
 
     /// <summary>
-    /// Copies an image as base64 string to the clipboard
+    ///     Copies an image as base64 string to the clipboard
     /// </summary>
     /// <param name="path">Optional path to the image file</param>
     /// <param name="vm">The main view model</param>
@@ -66,53 +66,63 @@ public static class ClipboardImageOperations
 
         return await ClipboardService.ExecuteClipboardOperation(async () =>
         {
-            var base64 = await GetBase64String(path, vm);
-            
-            if (string.IsNullOrEmpty(base64))
+            try
             {
+                var base64 = await GetBase64String(path, vm);
+
+                if (string.IsNullOrEmpty(base64))
+                {
+                    return false;
+                }
+
+                await clipboard.SetTextAsync(base64);
+                return true;
+            }
+            catch (Exception ex)
+            {
+#if DEBUG
+                Debug.WriteLine(
+                    $"{nameof(ClipboardImageOperations)} {nameof(CopyBase64ToClipboard)} error: {ex.Message}");
+                Debug.WriteLine(ex.StackTrace);
+#endif
                 return false;
             }
-
-            await clipboard.SetTextAsync(base64);
-            return true;
         });
     }
-    
+
     private static async Task<string> GetBase64String(string path, MainViewModel vm)
     {
-        if (string.IsNullOrWhiteSpace(path))
+        if (!string.IsNullOrWhiteSpace(path))
         {
-            switch (vm.ImageType)
-            {
-                case ImageType.AnimatedGif:
-                case ImageType.AnimatedWebp:
-                case ImageType.Bitmap:
-                    if (vm.ImageSource is not Bitmap bitmap)
-                    {
-                        return string.Empty;
-                    }
-
-                    using (var stream = new MemoryStream())
-                    {
-                        bitmap.Save(stream, quality: 100);
-                        return Convert.ToBase64String(stream.ToArray());
-                    }
-
-                case ImageType.Svg:
-                    return string.Empty;
-                    
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(vm.ImageType), $"Unsupported image type: {vm.ImageType}");
-            }
+            return Convert.ToBase64String(await File.ReadAllBytesAsync(path));
         }
-        else
+
+        switch (vm.ImageType)
         {
-            return Convert.ToBase64String(await File.ReadAllBytesAsync(path));
+            case ImageType.AnimatedGif:
+            case ImageType.AnimatedWebp:
+            case ImageType.Bitmap:
+                if (vm.ImageSource is not Bitmap bitmap)
+                {
+                    return string.Empty;
+                }
+
+                using (var stream = new MemoryStream())
+                {
+                    bitmap.Save(stream, 100);
+                    return Convert.ToBase64String(stream.ToArray());
+                }
+
+            case ImageType.Svg:
+                return string.Empty;
+
+            default:
+                throw new ArgumentOutOfRangeException(nameof(vm.ImageType), $"Unsupported image type: {vm.ImageType}");
         }
     }
-    
+
     /// <summary>
-    /// Pastes an image from the clipboard
+    ///     Pastes an image from the clipboard
     /// </summary>
     /// <param name="vm">The main view model</param>
     /// <returns>A task representing the asynchronous operation</returns>
@@ -127,11 +137,10 @@ public static class ClipboardImageOperations
         try
         {
             var name = TranslationHelper.Translation.ClipboardImage;
-            var imageType = ImageType.Bitmap;
 
             // Try standard image formats
             var bitmap = await TryGetBitmapFromClipboard(clipboard);
-            
+
             // Try Windows-specific clipboard handling if needed
             if (bitmap == null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             {
@@ -141,17 +150,18 @@ public static class ClipboardImageOperations
             // Set the image if we got one
             if (bitmap != null)
             {
-                await UpdateImage.SetSingleImageAsync(bitmap, imageType, name, vm);
+                await UpdateImage.SetSingleImageAsync(bitmap, ImageType.Bitmap, name, vm);
             }
         }
         catch (Exception ex)
         {
 #if DEBUG
             Debug.WriteLine($"{nameof(ClipboardImageOperations)} {nameof(PasteClipboardImage)} error: {ex.Message}");
+            Debug.WriteLine(ex.StackTrace);
 #endif
         }
     }
-    
+
     private static async Task<Bitmap?> TryGetBitmapFromClipboard(IClipboard clipboard)
     {
         // List of formats to try
@@ -174,12 +184,17 @@ public static class ClipboardImageOperations
                 using var memoryStream = new MemoryStream(dataBytes);
                 return new Bitmap(memoryStream);
             }
-            catch (Exception)
+            catch (Exception ex)
             {
                 // Ignore format errors and try next format
+#if DEBUG
+                Debug.WriteLine(
+                    $"{nameof(ClipboardImageOperations)} {nameof(TryGetBitmapFromClipboard)} error: {ex.Message}");
+                Debug.WriteLine(ex.StackTrace);
+#endif
             }
         }
-        
+
         return null;
     }
 }

+ 9 - 0
src/PicView.Avalonia/ColorManagement/BackgroundManager.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Media;
+using Avalonia.Threading;
 using PicView.Avalonia.ViewModels;
 
 namespace PicView.Avalonia.ColorManagement;
@@ -76,6 +77,14 @@ public static class BackgroundManager
         var nextChoice = (Settings.UIProperties.BgColorChoice + 1) % ((int)BackgroundType.MaxValue + 1);
         SetBackground(vm, nextChoice);
     }
+    
+    
+    /// <inheritdoc cref="ChangeBackground(MainViewModel)" />
+    public static async Task ChangeBackgroundAsync(MainViewModel vm)
+    {
+        await Dispatcher.UIThread.InvokeAsync(() => ChangeBackground(vm));
+        await SaveSettingsAsync();
+    }
 
     /// <summary>
     /// Sets the background of the view model based on the current background choice.

+ 26 - 79
src/PicView.Avalonia/UI/FunctionsHelper.cs

@@ -544,96 +544,50 @@ public static class FunctionsHelper
 
     #region File funnctions
 
-    public static async Task OpenLastFile()
-    {
-        if (Vm is null)
-        {
-            return;
-        }
-
+    public static async Task OpenLastFile() =>
         await NavigationManager.LoadPicFromStringAsync(FileHistory.GetLastEntry(), Vm).ConfigureAwait(false);
-    }
-
-    public static async Task OpenPreviousFileHistoryEntry()
-    {
-        if (Vm is null)
-        {
-            return;
-        }
 
+    public static async Task OpenPreviousFileHistoryEntry() =>
         await NavigationManager.LoadPicFromStringAsync(FileHistory.GetPreviousEntry(), Vm).ConfigureAwait(false);
-    }
-    public static async Task OpenNextFileHistoryEntry()
-    {
-        if (Vm is null)
-        {
-            return;
-        }
-
+    public static async Task OpenNextFileHistoryEntry() =>
         await NavigationManager.LoadPicFromStringAsync(FileHistory.GetNextEntry(), Vm).ConfigureAwait(false);
-    }
     
-    public static async Task Print() => await FileManager.Print(null, Vm).ConfigureAwait(false);
+    public static async Task Print() =>
+        await FileManager.Print(null, Vm).ConfigureAwait(false);
 
-    public static async Task Open()
-    {
-        await FilePicker.SelectAndLoadFile(Vm);
-    }
+    public static async Task Open() =>
+        await FilePicker.SelectAndLoadFile(Vm).ConfigureAwait(false);
 
-    public static Task OpenWith()
-    {
-        Vm?.PlatformService?.OpenWith(Vm.FileInfo?.FullName);
-        return Task.CompletedTask;
-    }
+    public static async Task OpenWith() =>
+        await Task.Run(() => Vm?.PlatformService?.OpenWith(Vm.FileInfo?.FullName)).ConfigureAwait(false);
+    
 
-    public static Task OpenInExplorer()
-    {
-        Vm?.PlatformService?.LocateOnDisk(Vm.FileInfo?.FullName);
-        return Task.CompletedTask;
-    }
+    public static async Task OpenInExplorer()=>
+        await Task.Run(() => Vm?.PlatformService?.LocateOnDisk(Vm.FileInfo?.FullName)).ConfigureAwait(false);
 
-    public static async Task Save()
-    {
-        await FileSaverHelper.SaveCurrentFile(Vm);
-    }
+    public static async Task Save() =>
+        await FileSaverHelper.SaveCurrentFile(Vm).ConfigureAwait(false);
     
-    public static async Task SaveAs()
-    {
-        await FileSaverHelper.SaveFileAs(Vm);
-    }
+    public static async Task SaveAs() =>
+        await FileSaverHelper.SaveFileAs(Vm).ConfigureAwait(false);
     
-    public static async Task DeleteFile()
-    {
-        if (Vm is null)
-        {
-            return;
-        }
-
-        await FileManager.DeleteFile(true, Vm);
-    }
+    public static async Task DeleteFile() =>
+        await FileManager.DeleteFile(true, Vm).ConfigureAwait(false);
     
-    public static async Task DeleteFilePermanently()
-    {
-        if (Vm is null)
-        {
-            return;
-        }
-
-        await FileManager.DeleteFile(false, Vm);
-    }
+    public static async Task DeleteFilePermanently() =>
+        await FileManager.DeleteFile(false, Vm).ConfigureAwait(false);
 
     public static async Task Rename()
     {
+        // TODO: Needs refactor
         await Dispatcher.UIThread.InvokeAsync(() =>
         {
             UIHelper.GetEditableTitlebar.SelectFileName();
         });
     }
     
-    public static async Task ShowFileProperties()
-    {
-        await Task.Run(() => Vm?.PlatformService?.ShowFileProperties(Vm.FileInfo?.FullName));
-    }
+    public static async Task ShowFileProperties() =>
+        await Task.Run(() => Vm?.PlatformService?.ShowFileProperties(Vm.FileInfo?.FullName)).ConfigureAwait(false);
     
     #endregion
 
@@ -664,16 +618,9 @@ public static class FunctionsHelper
 
     #region Image Functions
     
-    public static async Task ChangeBackground()
-    {
-        if (Vm is null)
-        {
-            return;
-        }
-        
-        BackgroundManager.ChangeBackground(Vm);
-        await SaveSettingsAsync();
-    }
+    /// <inheritdoc cref="BackgroundManager.ChangeBackground(MainViewModel)" />
+    public static async Task ChangeBackground() =>
+        await BackgroundManager.ChangeBackgroundAsync(Vm).ConfigureAwait(false);
     
     public static async Task SideBySide()
     {

+ 4 - 2
src/PicView.Avalonia/ViewModels/MainViewModel.cs

@@ -1501,9 +1501,11 @@ public class MainViewModel : ViewModelBase
     private async Task ShowFilePropertiesTask(string path) =>
         await FileManager.ShowFileProperties(path, this).ConfigureAwait(false);
 
-    private async Task PrintTask(string path) => await FileManager.Print(path, this).ConfigureAwait(false);
+    private async Task PrintTask(string path) =>
+        await FileManager.Print(path, this).ConfigureAwait(false);
 
-    private async Task OpenWithTask(string path) => await FileManager.OpenWith(path, this).ConfigureAwait(false);
+    private async Task OpenWithTask(string path) => 
+        await FileManager.OpenWith(path, this).ConfigureAwait(false);
 
     private async Task LocateOnDiskTask(string path) =>
         await FileManager.LocateOnDisk(path, this).ConfigureAwait(false);

+ 207 - 123
src/PicView.Core/Config/SettingsHelper.cs

@@ -11,90 +11,74 @@ internal partial class SourceGenerationContext : JsonSerializerContext;
 
 public static class SettingsHelper
 {
-    private const double CurrentSettingsVersion = 1.1;
-    private const string ConfigPath = "Config/UserSettings.json";
-    private const string RoamingConfigPath = "Ruben2776/PicView/Config/UserSettings.json";
+    private const double CurrentSettingsVersion = 1.3;
+    private const string ConfigFileName = "UserSettings.json";
+    private const string LocalConfigPath = "Config/" + ConfigFileName;
+    private const string RoamingConfigFolder = "Ruben2776/PicView/Config";
+    private const string RoamingConfigPath = RoamingConfigFolder + "/" + ConfigFileName;
 
     public static AppSettings? Settings { get; private set; }
-    
+
     /// <summary>
-    /// Asynchronously loads the user settings. Loads defaults if not found 
+    ///     Asynchronously loads the user settings. Loads defaults if not found
     /// </summary>
-    /// <returns>True if settings exists</returns>
+    /// <returns>True if settings exists and were loaded successfully</returns>
     public static async Task<bool> LoadSettingsAsync()
     {
         try
         {
             var path = GetUserSettingsPath();
-            if (string.IsNullOrEmpty(path))
+            if (!string.IsNullOrEmpty(path))
             {
-                SetDefaults();
-                return false;
+                return await LoadFromPathAsync(path).ConfigureAwait(false);
             }
 
-            try
-            {
-                await PerformRead(path).ConfigureAwait(false);
-            }
-            catch (Exception)
-            {
-                return await Retry().ConfigureAwait(false);
-            }
-        }
-#if DEBUG
-        catch (Exception ex)
-        {
-
-            Trace.WriteLine($"{nameof(LoadSettingsAsync)} error loading settings:\n {ex.Message}");
-
             SetDefaults();
             return false;
         }
-#else
-        catch
+        catch (Exception ex)
         {
-            SetDefaults();  
+            LogError(nameof(LoadSettingsAsync), ex);
+            SetDefaults();
             return false;
         }
-#endif
-        return true;
-
-        async Task<bool> Retry()
-        {
-            // TODO test saving location for macOS https://johnkoerner.com/csharp/special-folder-values-on-windows-versus-mac/
-            var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), RoamingConfigPath);
-            if (File.Exists(appData))
-            {
-                try
-                {
-                    await PerformRead(appData).ConfigureAwait(false);
-                }
-                catch (Exception)
-                {
-                    SetDefaults();
-                    return false;
-                }
-            }
-            else
-            {
-                SetDefaults();
-                return false;
-            }
-            return true;
-        }
     }
 
+    /// <summary>
+    ///     Determines the path to the user settings file
+    /// </summary>
+    /// <returns>Path to the user settings file, or empty string if not found</returns>
     public static string GetUserSettingsPath()
     {
-        var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), RoamingConfigPath);
-        if (File.Exists(appData))
+        var roamingPath = GetRoamingSettingsPath();
+        if (File.Exists(roamingPath))
         {
-            return appData;
+            return roamingPath;
         }
-        var baseDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigPath);
-        return File.Exists(baseDirectory) ? baseDirectory : string.Empty;
+
+        var localPath = GetLocalSettingsPath();
+        return File.Exists(localPath) ? localPath : string.Empty;
+    }
+
+    /// <summary>
+    ///     Gets the path to the roaming settings file
+    /// </summary>
+    private static string GetRoamingSettingsPath()
+    {
+        return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), RoamingConfigPath);
     }
 
+    /// <summary>
+    ///     Gets the path to the local settings file
+    /// </summary>
+    private static string GetLocalSettingsPath()
+    {
+        return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, LocalConfigPath);
+    }
+
+    /// <summary>
+    ///     Sets default settings values
+    /// </summary>
     public static void SetDefaults()
     {
         Settings = new AppSettings
@@ -106,139 +90,239 @@ public static class SettingsHelper
             Theme = new Theme(),
             WindowProperties = new WindowProperties(),
             Zoom = new Zoom(),
-            StartUp = new StartUp()
+            StartUp = new StartUp(),
+            Version = CurrentSettingsVersion
         };
         // Get the default culture from the OS
         Settings.UIProperties.UserLanguage = CultureInfo.CurrentCulture.Name;
     }
 
+    /// <summary>
+    ///     Deletes all settings files
+    /// </summary>
     public static void DeleteSettingFiles()
     {
         try
         {
-            var appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), RoamingConfigPath);
-            if (File.Exists(appDataPath))
-            {
-                File.Delete(appDataPath);
-            }
-
-            var baseDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigPath);
-            if (File.Exists(baseDirectory))
-            {
-                File.Delete(baseDirectory);
-            }
+            DeleteFileIfExists(GetRoamingSettingsPath());
+            DeleteFileIfExists(GetLocalSettingsPath());
         }
         catch (Exception ex)
         {
-#if DEBUG
-            Trace.WriteLine($"{nameof(DeleteSettingFiles)} error deleting settings:\n {ex.Message}");
-#endif
+            LogError(nameof(DeleteSettingFiles), ex);
+        }
+    }
+
+    /// <summary>
+    ///     Deletes a file if it exists
+    /// </summary>
+    private static void DeleteFileIfExists(string path)
+    {
+        if (File.Exists(path))
+        {
+            File.Delete(path);
         }
     }
 
-    private static async Task PerformSave(string path)
+    /// <summary>
+    ///     Loads settings from the specified path
+    /// </summary>
+    private static async Task<bool> LoadFromPathAsync(string path)
     {
-        var updatedJson = JsonSerializer.Serialize(
-            Settings, typeof(AppSettings), SourceGenerationContext.Default);
-        await using var writer = new StreamWriter(path);
-        await writer.WriteAsync(updatedJson).ConfigureAwait(false);
+        try
+        {
+            await ReadSettingsFromPathAsync(path).ConfigureAwait(false);
+            return true;
+        }
+        catch (Exception)
+        {
+            // If primary path fails, try the alternative path
+            var alternativePath = Path.GetDirectoryName(path)?.Contains("ApplicationData") == true
+                ? GetLocalSettingsPath()
+                : GetRoamingSettingsPath();
+
+            if (File.Exists(alternativePath))
+            {
+                try
+                {
+                    await ReadSettingsFromPathAsync(alternativePath).ConfigureAwait(false);
+                    return true;
+                }
+                catch (Exception)
+                {
+                    SetDefaults();
+                }
+            }
+            else
+            {
+                SetDefaults();
+            }
+
+            return false;
+        }
     }
 
-    private static async Task PerformRead(string path)
+    /// <summary>
+    ///     Reads settings from the specified path and upgrades them if necessary
+    /// </summary>
+    private static async Task ReadSettingsFromPathAsync(string path)
     {
         var jsonString = await File.ReadAllTextAsync(path).ConfigureAwait(false);
-        var settings = JsonSerializer.Deserialize(
-                jsonString, typeof(AppSettings), SourceGenerationContext.Default)
-            as AppSettings;
-        Settings = await UpgradeSettings(settings);
+
+        if (JsonSerializer.Deserialize(
+                jsonString, typeof(AppSettings), SourceGenerationContext.Default) is not AppSettings settings)
+        {
+            throw new JsonException("Failed to deserialize settings");
+        }
+
+        Settings = await UpgradeSettingsIfNeededAsync(settings).ConfigureAwait(false);
     }
 
+    /// <summary>
+    ///     Saves settings to disk
+    /// </summary>
     public static async Task<bool> SaveSettingsAsync()
     {
+        if (Settings == null)
+        {
+            return false;
+        }
+
         try
         {
-            var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config/UserSettings.json");
-            await PerformSave(path).ConfigureAwait(false);
+            // Try to save to local directory first
+            var localPath = GetLocalSettingsPath();
+            await SaveSettingsToPathAsync(localPath).ConfigureAwait(false);
+            return true;
         }
-        catch (UnauthorizedAccessException ex)
+        catch (UnauthorizedAccessException)
         {
-            var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Ruben2776/PicView/Config/UserSettings.json");
-            if (!File.Exists(path))
-            {
-                var fileInfo = new FileInfo(path);
-                fileInfo.Directory?.Create();
-            }
+            // If unauthorized, try saving to roaming app data
             try
             {
-                await PerformSave(path).ConfigureAwait(false);
+                var roamingPath = GetRoamingSettingsPath();
+                EnsureDirectoryExists(roamingPath);
+                await SaveSettingsToPathAsync(roamingPath).ConfigureAwait(false);
+                return true;
             }
-            catch (Exception)
+            catch (Exception ex)
             {
-                Trace.WriteLine($"{nameof(SaveSettingsAsync)} error saving settings:\n {ex.Message}");
+                LogError(nameof(SaveSettingsAsync), ex);
                 return false;
             }
         }
         catch (Exception ex)
         {
-            Trace.WriteLine($"{nameof(SaveSettingsAsync)} error saving settings:\n {ex.Message}");
+            LogError(nameof(SaveSettingsAsync), ex);
             return false;
         }
-        return true;
     }
 
-    private static async Task<AppSettings> UpgradeSettings(AppSettings settings)
+    /// <summary>
+    ///     Ensures that the directory for a file path exists
+    /// </summary>
+    private static void EnsureDirectoryExists(string filePath)
+    {
+        var directory = Path.GetDirectoryName(filePath);
+        if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+        {
+            Directory.CreateDirectory(directory);
+        }
+    }
+
+    /// <summary>
+    ///     Saves settings to the specified path
+    /// </summary>
+    private static async Task SaveSettingsToPathAsync(string path)
+    {
+        if (Settings == null)
+        {
+            return;
+        }
+
+        var json = JsonSerializer.Serialize(Settings, typeof(AppSettings), SourceGenerationContext.Default);
+        await File.WriteAllTextAsync(path, json).ConfigureAwait(false);
+    }
+
+    /// <summary>
+    ///     Upgrades settings to the current version if needed
+    /// </summary>
+    private static async Task<AppSettings> UpgradeSettingsIfNeededAsync(AppSettings settings)
     {
         if (settings.Version >= CurrentSettingsVersion)
         {
             return settings;
         }
 
-        await SynchronizeSettings(settings).ConfigureAwait(false);
-
+        await SynchronizeSettingsAsync(settings).ConfigureAwait(false);
         settings.Version = CurrentSettingsVersion;
 
         return settings;
     }
 
-    private static async Task SynchronizeSettings(AppSettings settings)
+    /// <summary>
+    ///     Synchronizes settings between different versions
+    /// </summary>
+    private static async Task SynchronizeSettingsAsync(AppSettings newSettings)
     {
         try
         {
-            var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config/UserSettings.json");
-            if (!File.Exists(path))
+            var localPath = GetLocalSettingsPath();
+            if (!File.Exists(localPath))
             {
                 return;
             }
 
-            var jsonString = await File.ReadAllTextAsync(path).ConfigureAwait(false);
+            var jsonString = await File.ReadAllTextAsync(localPath).ConfigureAwait(false);
+            var existingSettings = JsonSerializer.Deserialize(
+                jsonString, typeof(AppSettings), SourceGenerationContext.Default) as AppSettings;
 
-            if (JsonSerializer.Deserialize(
-                    jsonString, typeof(AppSettings), SourceGenerationContext.Default) is not AppSettings existingSettings)
+            if (existingSettings == null)
             {
-                SetDefaults();
                 return;
             }
 
-            var properties = typeof(AppSettings).GetProperties();
-            foreach (var property in properties)
-            {
-                // Check if the property exists in the existing JSON
-                var jsonProperty = typeof(AppSettings).GetProperty(property.Name);
-                if (jsonProperty == null)
-                {
-                    // Property exists in C# class but not in JSON, initialize it
-                    property.SetValue(existingSettings, property.GetValue(settings));
-                }
-            }
+            // Copy new property values to existing settings when missing
+            MergeSettings(existingSettings, newSettings);
 
-            // Save the synchronized settings back to the JSON file
-            var updatedJson = JsonSerializer.Serialize(
-                existingSettings, typeof(AppSettings), SourceGenerationContext.Default);
-            await File.WriteAllTextAsync(path, updatedJson).ConfigureAwait(false);
+            // Save the synchronized settings
+            Settings = existingSettings;
+            await SaveSettingsToPathAsync(localPath).ConfigureAwait(false);
         }
         catch (Exception ex)
         {
-            Trace.WriteLine($"{nameof(SynchronizeSettings)} error synchronizing settings:\n {ex.Message}");
+            LogError(nameof(SynchronizeSettingsAsync), ex);
         }
     }
+
+    /// <summary>
+    ///     Merges settings by copying properties from newSettings to existingSettings
+    ///     where the property is missing or null in existingSettings
+    /// </summary>
+    private static void MergeSettings(AppSettings existingSettings, AppSettings newSettings)
+    {
+        foreach (var property in typeof(AppSettings).GetProperties())
+        {
+            var existingValue = property.GetValue(existingSettings);
+
+            if (existingValue != null)
+            {
+                continue;
+            }
+
+            var newValue = property.GetValue(newSettings);
+            property.SetValue(existingSettings, newValue);
+        }
+    }
+
+    /// <summary>
+    ///     Logs an error message
+    /// </summary>
+    private static void LogError(string methodName, Exception ex)
+    {
+#if DEBUG
+        Trace.WriteLine($"{nameof(SettingsHelper)}: {methodName} error: {ex.Message}");
+        Trace.WriteLine(ex.StackTrace);
+#endif
+    }
 }