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.Interop; using CursorLang.Models; using Windows.Storage; 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 const string FileName = "settings.json"; private readonly string _filePath; private readonly string _inheritedFilePath; private readonly DispatcherTimer _saveTimer; private AppSettings? _settings; // Sliders change their values continuously, so writing to disk // is postponed until there is a pause in the changes private static readonly TimeSpan SaveDelay = TimeSpan.FromMilliseconds(500); public SettingsService() : this( Path.Combine(GetSettingsFolder(), FileName), Path.Combine(GetSeparateInstallFolder(), FileName), SaveDelay) { } /// /// Sets the storage locations and the save delay explicitly — thereby making it /// possible to check the work with the file without touching the settings of the /// user themselves. /// internal SettingsService(string filePath, string inheritedFilePath, TimeSpan saveDelay) { _filePath = filePath; _inheritedFilePath = inheritedFilePath; _saveTimer = new DispatcherTimer { Interval = saveDelay }; _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() { AppSettings? stored = ReadFile(_filePath); // There is no file of our own — the application may well have been configured // before the move to a package. Taking the settings from there beats starting // from a blank slate bool inherited = stored is null && _filePath != _inheritedFilePath; if (inherited) { stored = ReadFile(_inheritedFilePath); inherited = stored is not null; } _settings = stored ?? CreateDefault(); _settings.PropertyChanged += OnSettingsChanged; // Moved settings are fixed in the new place right away rather than on the // first edit: otherwise the application would read someone else's file every // time until then if (inherited) { Save(); } 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(); } } /// /// The folder the application writes its settings to. /// private static string GetSettingsFolder() { if (!PackageIdentityNative.IsPackaged) { return GetSeparateInstallFolder(); } // A package has a data folder of its own, which Windows creates and removes // itself. The application name is not appended to it: the folder belongs to it alone anyway return ApplicationData.Current.LocalFolder.Path; } /// /// The settings folder of a separately installed application — the same source /// the package inherits the settings from on the first launch. /// private static string GetSeparateInstallFolder() => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CursorLang"); private static AppSettings? ReadFile(string path) { try { if (!File.Exists(path)) { return null; } return JsonSerializer.Deserialize(File.ReadAllText(path), 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()); } } }