|
4 | 4 | using System.Text.Json;
|
5 | 5 |
|
6 | 6 | namespace Coder.Desktop.App.Services;
|
| 7 | + |
7 | 8 | /// <summary>
|
8 |
| -/// Generic persistence contract for simple key/value settings. |
| 9 | +/// Settings contract exposing properties for app settings. |
9 | 10 | /// </summary>
|
10 | 11 | public interface ISettingsManager
|
11 | 12 | {
|
12 | 13 | /// <summary>
|
13 |
| - /// Saves <paramref name="value"/> under <paramref name="name"/> and returns the value. |
| 14 | + /// Returns the value of the StartOnLogin setting. Returns <c>false</c> if the key is not found. |
14 | 15 | /// </summary>
|
15 |
| - T Save<T>(string name, T value); |
| 16 | + bool StartOnLogin { get; set; } |
16 | 17 |
|
17 | 18 | /// <summary>
|
18 |
| - /// Reads the setting or returns <paramref name="defaultValue"/> when the key is missing. |
| 19 | + /// Returns the value of the ConnectOnLaunch setting. Returns <c>false</c> if the key is not found. |
19 | 20 | /// </summary>
|
20 |
| - T Read<T>(string name, T defaultValue); |
| 21 | + bool ConnectOnLaunch { get; set; } |
21 | 22 | }
|
| 23 | + |
22 | 24 | /// <summary>
|
23 |
| -/// JSON‑file implementation that works in unpackaged Win32/WinUI 3 apps. |
| 25 | +/// Implemention of <see cref="ISettingsManager"/> that persists settings to a JSON file |
| 26 | +/// located in the user's local application data folder. |
24 | 27 | /// </summary>
|
25 | 28 | public sealed class SettingsManager : ISettingsManager
|
26 | 29 | {
|
27 | 30 | private readonly string _settingsFilePath;
|
28 | 31 | private readonly string _fileName = "app-settings.json";
|
| 32 | + private readonly string _appName = "CoderDesktop"; |
29 | 33 | private readonly object _lock = new();
|
30 | 34 | private Dictionary<string, JsonElement> _cache;
|
31 | 35 |
|
32 |
| - public static readonly string ConnectOnLaunchKey = "ConnectOnLaunch"; |
33 |
| - public static readonly string StartOnLoginKey = "StartOnLogin"; |
| 36 | + public const string ConnectOnLaunchKey = "ConnectOnLaunch"; |
| 37 | + public const string StartOnLoginKey = "StartOnLogin"; |
34 | 38 |
|
35 |
| - /// <param name="appName"> |
36 |
| - /// Sub‑folder under %LOCALAPPDATA% (e.g. "CoderDesktop"). |
37 |
| - /// If <c>null</c> the folder name defaults to the executable name. |
| 39 | + public bool StartOnLogin |
| 40 | + { |
| 41 | + get |
| 42 | + { |
| 43 | + return Read(StartOnLoginKey, false); |
| 44 | + } |
| 45 | + set |
| 46 | + { |
| 47 | + Save(StartOnLoginKey, value); |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + public bool ConnectOnLaunch |
| 52 | + { |
| 53 | + get |
| 54 | + { |
| 55 | + return Read(ConnectOnLaunchKey, false); |
| 56 | + } |
| 57 | + set |
| 58 | + { |
| 59 | + Save(ConnectOnLaunchKey, value); |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + /// <param name="settingsFilePath"> |
38 | 64 | /// For unit‑tests you can pass an absolute path that already exists.
|
| 65 | + /// Otherwise the settings file will be created in the user's local application data folder. |
39 | 66 | /// </param>
|
40 |
| - public SettingsManager(string? appName = null) |
| 67 | + public SettingsManager(string? settingsFilePath = null) |
41 | 68 | {
|
42 |
| - // Allow unit‑tests to inject a fully‑qualified path. |
43 |
| - if (appName is not null && Path.IsPathRooted(appName)) |
| 69 | + if (settingsFilePath is null) |
44 | 70 | {
|
45 |
| - _settingsFilePath = Path.Combine(appName, _fileName); |
46 |
| - Directory.CreateDirectory(Path.GetDirectoryName(_settingsFilePath)!); |
| 71 | + settingsFilePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); |
47 | 72 | }
|
48 |
| - else |
| 73 | + else if (!Path.IsPathRooted(settingsFilePath)) |
49 | 74 | {
|
50 |
| - string folder = Path.Combine( |
51 |
| - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), |
52 |
| - appName ?? AppDomain.CurrentDomain.FriendlyName.ToLowerInvariant()); |
53 |
| - Directory.CreateDirectory(folder); |
54 |
| - _settingsFilePath = Path.Combine(folder, _fileName); |
| 75 | + throw new ArgumentException("settingsFilePath must be an absolute path if provided", nameof(settingsFilePath)); |
| 76 | + } |
| 77 | + |
| 78 | + string folder = Path.Combine( |
| 79 | + settingsFilePath, |
| 80 | + _appName); |
| 81 | + |
| 82 | + Directory.CreateDirectory(folder); |
| 83 | + _settingsFilePath = Path.Combine(folder, _fileName); |
| 84 | + |
| 85 | + if(!File.Exists(_settingsFilePath)) |
| 86 | + { |
| 87 | + // Create the settings file if it doesn't exist |
| 88 | + string emptyJson = JsonSerializer.Serialize(new { }); |
| 89 | + File.WriteAllText(_settingsFilePath, emptyJson); |
55 | 90 | }
|
56 | 91 |
|
57 | 92 | _cache = Load();
|
58 | 93 | }
|
59 | 94 |
|
60 |
| - public T Save<T>(string name, T value) |
| 95 | + private void Save(string name, bool value) |
61 | 96 | {
|
62 | 97 | lock (_lock)
|
63 | 98 | {
|
64 |
| - _cache[name] = JsonSerializer.SerializeToElement(value); |
65 |
| - Persist(); |
66 |
| - return value; |
| 99 | + try |
| 100 | + { |
| 101 | + // Ensure cache is loaded before saving |
| 102 | + using var fs = new FileStream(_settingsFilePath, |
| 103 | + FileMode.OpenOrCreate, |
| 104 | + FileAccess.ReadWrite, |
| 105 | + FileShare.None); |
| 106 | + |
| 107 | + var currentCache = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(fs) ?? new(); |
| 108 | + _cache = currentCache; |
| 109 | + _cache[name] = JsonSerializer.SerializeToElement(value); |
| 110 | + fs.Position = 0; // Reset stream position to the beginning before writing to override the file |
| 111 | + var options = new JsonSerializerOptions { WriteIndented = true}; |
| 112 | + JsonSerializer.Serialize(fs, _cache, options); |
| 113 | + } |
| 114 | + catch |
| 115 | + { |
| 116 | + throw new InvalidOperationException($"Failed to persist settings to {_settingsFilePath}. The file may be corrupted, malformed or locked."); |
| 117 | + } |
67 | 118 | }
|
68 | 119 | }
|
69 | 120 |
|
70 |
| - public T Read<T>(string name, T defaultValue) |
| 121 | + private bool Read(string name, bool defaultValue) |
71 | 122 | {
|
72 | 123 | lock (_lock)
|
73 | 124 | {
|
74 | 125 | if (_cache.TryGetValue(name, out var element))
|
75 | 126 | {
|
76 | 127 | try
|
77 | 128 | {
|
78 |
| - return element.Deserialize<T>() ?? defaultValue; |
| 129 | + return element.Deserialize<bool?>() ?? defaultValue; |
79 | 130 | }
|
80 | 131 | catch
|
81 | 132 | {
|
82 |
| - // Malformed value – fall back. |
| 133 | + // malformed value – return default value |
83 | 134 | return defaultValue;
|
84 | 135 | }
|
85 | 136 | }
|
86 |
| - return defaultValue; // key not found – return caller‑supplied default (false etc.) |
| 137 | + return defaultValue; // key not found – return default value |
87 | 138 | }
|
88 | 139 | }
|
89 | 140 |
|
90 | 141 | private Dictionary<string, JsonElement> Load()
|
91 | 142 | {
|
92 |
| - if (!File.Exists(_settingsFilePath)) |
93 |
| - return new(); |
94 |
| - |
95 | 143 | try
|
96 | 144 | {
|
97 | 145 | using var fs = File.OpenRead(_settingsFilePath);
|
98 | 146 | return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(fs) ?? new();
|
99 | 147 | }
|
100 |
| - catch |
| 148 | + catch (Exception ex) |
101 | 149 | {
|
102 |
| - // Corrupted file – start fresh. |
103 |
| - return new(); |
| 150 | + throw new InvalidOperationException($"Failed to load settings from {_settingsFilePath}. The file may be corrupted or malformed. Exception: {ex.Message}"); |
104 | 151 | }
|
105 | 152 | }
|
106 |
| - |
107 |
| - private void Persist() |
108 |
| - { |
109 |
| - using var fs = File.Create(_settingsFilePath); |
110 |
| - var options = new JsonSerializerOptions { WriteIndented = true }; |
111 |
| - JsonSerializer.Serialize(fs, _cache, options); |
112 |
| - } |
113 | 153 | }
|
0 commit comments