modified settings services
This commit is contained in:
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user