modified settings services

This commit is contained in:
2026-08-09 20:14:43 +05:00
parent 1053932cfd
commit 81684b55c9
2 changed files with 141 additions and 14 deletions
+75 -13
View File
@@ -4,7 +4,9 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
using CursorLang.Interop;
using CursorLang.Models; using CursorLang.Models;
using Windows.Storage;
namespace CursorLang.Services; namespace CursorLang.Services;
@@ -31,20 +33,36 @@ public sealed class SettingsService : IDisposable
Converters = { new ColorJsonConverter(), new JsonStringEnumConverter() }, Converters = { new ColorJsonConverter(), new JsonStringEnumConverter() },
}; };
private const string FileName = "settings.json";
private readonly string _filePath; private readonly string _filePath;
private readonly string _inheritedFilePath;
private readonly DispatcherTimer _saveTimer; private readonly DispatcherTimer _saveTimer;
private AppSettings? _settings; private AppSettings? _settings;
public SettingsService() // Sliders change their values continuously, so writing to disk
{ // is postponed until there is a pause in the changes
string folder = Path.Combine( private static readonly TimeSpan SaveDelay = TimeSpan.FromMilliseconds(500);
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"CursorLang");
_filePath = Path.Combine(folder, "settings.json");
// Ползунки меняют значения непрерывно, поэтому запись на диск public SettingsService()
// откладывается до паузы в изменениях : this(
_saveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; 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; _saveTimer.Tick += OnSaveTimerTick;
} }
@@ -54,8 +72,29 @@ public sealed class SettingsService : IDisposable
/// </summary> /// </summary>
public AppSettings Load() public AppSettings Load()
{ {
_settings = ReadFile() ?? CreateDefault(); 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; _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; return _settings;
} }
@@ -89,16 +128,39 @@ public sealed class SettingsService : IDisposable
} }
} }
private AppSettings? ReadFile() /// <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 try
{ {
if (!File.Exists(_filePath)) if (!File.Exists(path))
{ {
return null; return null;
} }
return JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(_filePath), SerializerOptions); return JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(path), SerializerOptions);
} }
catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException) catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
{ {
+66 -1
View File
@@ -64,10 +64,20 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
Color.FromRgb(0xF8, 0x71, 0x71), Color.FromRgb(0xF8, 0x71, 0x71),
]; ];
public SettingsViewModel(AppSettings settings, ILocalizationService localization) private readonly IStartupService _startup;
private StartupState _startupState = StartupState.Unavailable;
public SettingsViewModel(
AppSettings settings,
ILocalizationService localization,
IStartupService startup,
UpdateViewModel updates)
{ {
Settings = settings; Settings = settings;
Localization = localization; Localization = localization;
Updates = updates;
_startup = startup;
// The interface language is a setting like any other and is stored in the same place // The interface language is a setting like any other and is stored in the same place
Localization.CurrentLanguage = settings.Language; Localization.CurrentLanguage = settings.Language;
@@ -84,6 +94,9 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
public ILocalizationService Localization { get; } public ILocalizationService Localization { get; }
/// <summary>The updates section: it has a state and commands of its own.</summary>
public UpdateViewModel Updates { get; }
public IReadOnlyList<Color> BackgroundPalette { get; } = Palette; public IReadOnlyList<Color> BackgroundPalette { get; } = Palette;
public IReadOnlyList<Color> TextPalette { get; } = ForegroundPalette; public IReadOnlyList<Color> TextPalette { get; } = ForegroundPalette;
@@ -96,12 +109,64 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
public IReadOnlyList<EnumOption<ScreenPosition>> ScreenPositions { get; } public IReadOnlyList<EnumOption<ScreenPosition>> ScreenPositions { get; }
/// <summary>Whether to show the startup setting.</summary>
public bool IsStartupAvailable => _startupState != StartupState.Unavailable;
/// <summary>Startup is up to the application rather than to Windows.</summary>
public bool CanChangeStartup => _startupState is StartupState.Enabled or StartupState.Disabled;
/// <summary>It has to be explained why the setting does not give in.</summary>
public bool IsStartupLocked => IsStartupAvailable && !CanChangeStartup;
/// <summary>
/// Start the application together with Windows.
/// </summary>
/// <remarks>
/// The value is not stored in the application settings: Windows knows it, and it
/// may be changed behind our back — in the "Startup apps" section. So the property
/// answers from the last known state of the task every time.
/// </remarks>
public bool RunAtStartup
{
get => _startupState is StartupState.Enabled or StartupState.EnabledByPolicy;
set
{
if (value == RunAtStartup)
{
return;
}
_ = ApplyStartupAsync(value);
}
}
/// <summary>
/// Asks Windows about the state of startup. Called after the window is shown: the
/// answer has to be waited for, while the settings must open right away.
/// </summary>
public async Task InitializeAsync() => UpdateStartupState(await _startup.GetStateAsync());
public void Dispose() public void Dispose()
{ {
Settings.PropertyChanged -= OnSettingsChanged; Settings.PropertyChanged -= OnSettingsChanged;
Localization.PropertyChanged -= OnLocalizationChanged; Localization.PropertyChanged -= OnLocalizationChanged;
} }
private async Task ApplyStartupAsync(bool enabled) =>
UpdateStartupState(await _startup.SetEnabledAsync(enabled));
private void UpdateStartupState(StartupState state)
{
_startupState = state;
// The change is reported even when the state is the same: the checkbox has
// already been toggled, and only a re-read value can put it back
OnPropertyChanged(nameof(RunAtStartup));
OnPropertyChanged(nameof(IsStartupAvailable));
OnPropertyChanged(nameof(CanChangeStartup));
OnPropertyChanged(nameof(IsStartupLocked));
}
private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e) private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(AppSettings.Language)) if (e.PropertyName == nameof(AppSettings.Language))