using System.ComponentModel; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; using System.Windows.Media; using System.Windows.Threading; using CursorLang.Models; namespace CursorLang.Services; /// /// Keeps the settings in the settings.json file. /// /// /// The location of the file depends on how the application is installed. A package /// from the Store keeps its settings in a folder of its own: Windows removes it /// together with the application, and after the removal nothing superfluous is left /// in the system — that is what Store applications are expected to do. A separately /// installed application keeps its settings in %APPDATA%, as before. /// /// Settings left over from a separately installed application are picked up by the /// package on the first launch and moved over. The original file stays where it is: /// both versions can be installed side by side, and the application has no right to /// delete settings that are not its own. /// public sealed class SettingsService : IDisposable { private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true, Converters = { new ColorJsonConverter(), new JsonStringEnumConverter() }, }; private readonly string _filePath; private readonly DispatcherTimer _saveTimer; private AppSettings? _settings; public SettingsService() { string folder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CursorLang"); _filePath = Path.Combine(folder, "settings.json"); // Ползунки меняют значения непрерывно, поэтому запись на диск // откладывается до паузы в изменениях _saveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _saveTimer.Tick += OnSaveTimerTick; } /// /// Reads the settings from disk or returns the default values, /// and from then on saves any changes by itself. /// public AppSettings Load() { _settings = ReadFile() ?? CreateDefault(); _settings.PropertyChanged += OnSettingsChanged; return _settings; } public void Save() { if (_settings is null) { return; } try { Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); File.WriteAllText(_filePath, JsonSerializer.Serialize(_settings, SerializerOptions)); } catch (Exception e) when (e is IOException or UnauthorizedAccessException) { // The settings are not the kind of thing worth bringing the application down for } } public void Dispose() { _saveTimer.Stop(); _saveTimer.Tick -= OnSaveTimerTick; if (_settings is not null) { _settings.PropertyChanged -= OnSettingsChanged; Save(); } } private AppSettings? ReadFile() { try { if (!File.Exists(_filePath)) { return null; } return JsonSerializer.Deserialize(File.ReadAllText(_filePath), SerializerOptions); } catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException) { return null; } } private static AppSettings CreateDefault() { string uiLanguage = System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; return new AppSettings { Language = uiLanguage == "ru" ? "ru" : "en" }; } private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e) { _saveTimer.Stop(); _saveTimer.Start(); } private void OnSaveTimerTick(object? sender, EventArgs e) { _saveTimer.Stop(); Save(); } // Color is not serialized out of the box, and keeping it readable in the file is handy private sealed class ColorJsonConverter : JsonConverter { public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { string? value = reader.GetString(); if (string.IsNullOrWhiteSpace(value)) { return Colors.Black; } try { return (Color)ColorConverter.ConvertFromString(value)!; } catch (FormatException) { return Colors.Black; } } public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); } } }