From 81684b55c97402520fe0ddef2201dbf1db4ae9a5 Mon Sep 17 00:00:00 2001 From: Aleksandr Neychev Date: Sun, 9 Aug 2026 20:14:43 +0500 Subject: [PATCH] modified settings services --- CursorLang/Services/SettingsService.cs | 88 ++++++++++++++++++---- CursorLang/ViewModels/SettingsViewModel.cs | 67 +++++++++++++++- 2 files changed, 141 insertions(+), 14 deletions(-) diff --git a/CursorLang/Services/SettingsService.cs b/CursorLang/Services/SettingsService.cs index 1dd0ff0..96d6909 100644 --- a/CursorLang/Services/SettingsService.cs +++ b/CursorLang/Services/SettingsService.cs @@ -4,7 +4,9 @@ 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; @@ -31,20 +33,36 @@ public sealed class SettingsService : IDisposable 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; - public SettingsService() - { - string folder = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "CursorLang"); - _filePath = Path.Combine(folder, "settings.json"); + // 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); - // Ползунки меняют значения непрерывно, поэтому запись на диск - // откладывается до паузы в изменениях - _saveTimer = new DispatcherTimer { Interval = 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; } @@ -54,8 +72,29 @@ public sealed class SettingsService : IDisposable /// 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; + + // 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; } @@ -89,16 +128,39 @@ public sealed class SettingsService : IDisposable } } - private AppSettings? ReadFile() + /// + /// 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(_filePath)) + if (!File.Exists(path)) { return null; } - return JsonSerializer.Deserialize(File.ReadAllText(_filePath), SerializerOptions); + return JsonSerializer.Deserialize(File.ReadAllText(path), SerializerOptions); } catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException) { diff --git a/CursorLang/ViewModels/SettingsViewModel.cs b/CursorLang/ViewModels/SettingsViewModel.cs index 70361f7..c19afcc 100644 --- a/CursorLang/ViewModels/SettingsViewModel.cs +++ b/CursorLang/ViewModels/SettingsViewModel.cs @@ -64,10 +64,20 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable 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; Localization = localization; + Updates = updates; + _startup = startup; // The interface language is a setting like any other and is stored in the same place Localization.CurrentLanguage = settings.Language; @@ -84,6 +94,9 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable public ILocalizationService Localization { get; } + /// The updates section: it has a state and commands of its own. + public UpdateViewModel Updates { get; } + public IReadOnlyList BackgroundPalette { get; } = Palette; public IReadOnlyList TextPalette { get; } = ForegroundPalette; @@ -96,12 +109,64 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable public IReadOnlyList> ScreenPositions { get; } + /// Whether to show the startup setting. + public bool IsStartupAvailable => _startupState != StartupState.Unavailable; + + /// Startup is up to the application rather than to Windows. + public bool CanChangeStartup => _startupState is StartupState.Enabled or StartupState.Disabled; + + /// It has to be explained why the setting does not give in. + public bool IsStartupLocked => IsStartupAvailable && !CanChangeStartup; + + /// + /// Start the application together with Windows. + /// + /// + /// 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. + /// + public bool RunAtStartup + { + get => _startupState is StartupState.Enabled or StartupState.EnabledByPolicy; + set + { + if (value == RunAtStartup) + { + return; + } + + _ = ApplyStartupAsync(value); + } + } + + /// + /// 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. + /// + public async Task InitializeAsync() => UpdateStartupState(await _startup.GetStateAsync()); + public void Dispose() { Settings.PropertyChanged -= OnSettingsChanged; 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) { if (e.PropertyName == nameof(AppSettings.Language))