216 lines
6.9 KiB
C#
216 lines
6.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Keeps the settings in the settings.json file.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
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)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal SettingsService(string filePath, string inheritedFilePath, TimeSpan saveDelay)
|
|
{
|
|
_filePath = filePath;
|
|
_inheritedFilePath = inheritedFilePath;
|
|
|
|
_saveTimer = new DispatcherTimer { Interval = saveDelay };
|
|
_saveTimer.Tick += OnSaveTimerTick;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the settings from disk or returns the default values,
|
|
/// and from then on saves any changes by itself.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The folder the application writes its settings to.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The settings folder of a separately installed application — the same source
|
|
/// the package inherits the settings from on the first launch.
|
|
/// </summary>
|
|
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<AppSettings>(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<Color>
|
|
{
|
|
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());
|
|
}
|
|
}
|
|
}
|