From 24ad9e15c681cc25236d7e45e06e1abf65f041f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:08:29 +0000 Subject: [PATCH 01/25] Initial plan From 122558723af9a278b7e7cf33f6dc6696e1802c62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:11:14 +0000 Subject: [PATCH 02/25] Add policy to override SettingsFolder Location Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Settings/PolicyInfo.cs | 3 ++ .../NETworkManager.Settings/PolicyManager.cs | 1 + .../SettingsManager.cs | 5 ++ .../config.json.example | 3 +- Website/docs/settings/settings.md | 4 ++ Website/docs/system-wide-policies.md | 49 ++++++++++++++++++- 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Source/NETworkManager.Settings/PolicyInfo.cs b/Source/NETworkManager.Settings/PolicyInfo.cs index 7b9b64a6a0..0a7f512ba3 100644 --- a/Source/NETworkManager.Settings/PolicyInfo.cs +++ b/Source/NETworkManager.Settings/PolicyInfo.cs @@ -10,4 +10,7 @@ public class PolicyInfo { [JsonPropertyName("Update_CheckForUpdatesAtStartup")] public bool? Update_CheckForUpdatesAtStartup { get; set; } + + [JsonPropertyName("SettingsFolderLocation")] + public string SettingsFolderLocation { get; set; } } diff --git a/Source/NETworkManager.Settings/PolicyManager.cs b/Source/NETworkManager.Settings/PolicyManager.cs index 7dd762c8de..fc946620bc 100644 --- a/Source/NETworkManager.Settings/PolicyManager.cs +++ b/Source/NETworkManager.Settings/PolicyManager.cs @@ -83,6 +83,7 @@ public static void Load() // Log enabled settings Log.Info($"System-wide policy - Update_CheckForUpdatesAtStartup: {Current.Update_CheckForUpdatesAtStartup?.ToString() ?? "Not set"}"); + Log.Info($"System-wide policy - SettingsFolderLocation: {Current.SettingsFolderLocation ?? "Not set"}"); } } catch (Exception ex) diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 29a140218e..83241bde51 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -77,6 +77,11 @@ public static class SettingsManager /// Path to the settings folder. public static string GetSettingsFolderLocation() { + // Policy override takes precedence + if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation)) + return PolicyManager.Current.SettingsFolderLocation; + + // Fall back to existing logic return ConfigurationManager.Current.IsPortable ? Path.Combine(AssemblyManager.Current.Location, SettingsFolderName) : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), diff --git a/Source/NETworkManager.Settings/config.json.example b/Source/NETworkManager.Settings/config.json.example index 8ca2bf05b3..0a47ffa700 100644 --- a/Source/NETworkManager.Settings/config.json.example +++ b/Source/NETworkManager.Settings/config.json.example @@ -1,3 +1,4 @@ { - "Update_CheckForUpdatesAtStartup": false + "Update_CheckForUpdatesAtStartup": false, + "SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings" } \ No newline at end of file diff --git a/Website/docs/settings/settings.md b/Website/docs/settings/settings.md index 32342b3d6e..f4404603c2 100644 --- a/Website/docs/settings/settings.md +++ b/Website/docs/settings/settings.md @@ -17,6 +17,10 @@ Folder where the application settings are stored. | Setup / Archiv | `%UserProfile%\Documents\NETworkManager\Settings` | | Portable | `\Settings` | +**Policy Override**: `SettingsFolderLocation` + +This setting can be overridden by a [system-wide policy](../system-wide-policies.md). When a policy is configured, it takes precedence over the default location, allowing administrators to specify a custom settings folder path for all users. + :::note **Recommendation** diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md index d8ef644453..56e85d6b5d 100644 --- a/Website/docs/system-wide-policies.md +++ b/Website/docs/system-wide-policies.md @@ -36,7 +36,8 @@ The `config.json` file uses a simple JSON structure to define policy values. An ```json { - "Update_CheckForUpdatesAtStartup": false + "Update_CheckForUpdatesAtStartup": false, + "SettingsFolderLocation": "C:\\ProgramData\\NETworkManager\\Settings" } ``` @@ -97,6 +98,52 @@ Ensure the `config.json` file has appropriate permissions so that regular users - Verify the value is a boolean (`true` or `false`), not a string (`"true"` or `"false"`) - Check that there are no syntax errors in the JSON file +## Available Policies + +### Update_CheckForUpdatesAtStartup + +Control whether the application checks for updates at startup. + +**Type**: `Boolean` (true/false) + +**Default**: User setting applies + +**Example**: +```json +{ + "Update_CheckForUpdatesAtStartup": false +} +``` + +See [Check for updates at startup](./settings/update.md#check-for-updates-at-startup) for more details. + +### SettingsFolderLocation + +Override the default location where application settings are stored. + +**Type**: `String` (absolute path) + +**Default**: User Documents folder or portable location + +**Example**: +```json +{ + "SettingsFolderLocation": "C:\\ProgramData\\NETworkManager\\Settings" +} +``` + +See [Location](./settings/settings.md#location) for more details. + +:::warning + +When configuring a custom settings folder location, ensure: +- The path uses double backslashes (`\\`) in JSON format +- The application has read/write permissions to the specified directory +- The directory exists or the application has permissions to create it +- All users have appropriate access to the folder if using a shared location + +::: + ## Future Policies Additional policy options will be added in future releases to provide more granular control over application behavior. If you have specific requirements for system-wide policies in your organization, please submit a feature request via the [GitHub issue tracker](https://github.com/BornToBeRoot/NETworkManager/issues/new/choose). From bf0c78e8df2ec8d14ab38a4e760ef18d194b3419 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:13:00 +0000 Subject: [PATCH 03/25] Add validation for policy-provided SettingsFolderLocation Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Settings/PolicyInfo.cs | 2 +- .../SettingsManager.cs | 24 ++++++++++++++++++- Website/docs/system-wide-policies.md | 4 ++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Source/NETworkManager.Settings/PolicyInfo.cs b/Source/NETworkManager.Settings/PolicyInfo.cs index 0a7f512ba3..2f17ee8abf 100644 --- a/Source/NETworkManager.Settings/PolicyInfo.cs +++ b/Source/NETworkManager.Settings/PolicyInfo.cs @@ -12,5 +12,5 @@ public class PolicyInfo public bool? Update_CheckForUpdatesAtStartup { get; set; } [JsonPropertyName("SettingsFolderLocation")] - public string SettingsFolderLocation { get; set; } + public string? SettingsFolderLocation { get; set; } } diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 83241bde51..1343bf2c81 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -79,7 +79,29 @@ public static string GetSettingsFolderLocation() { // Policy override takes precedence if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation)) - return PolicyManager.Current.SettingsFolderLocation; + { + var policyPath = PolicyManager.Current.SettingsFolderLocation; + + // Validate that the policy-provided path is rooted (absolute) + if (!Path.IsPathRooted(policyPath)) + { + Log.Error($"Policy-provided SettingsFolderLocation is not an absolute path: {policyPath}. Falling back to default location."); + } + else + { + // Validate that the path doesn't contain invalid characters + try + { + // This will throw if the path contains invalid characters + _ = Path.GetFullPath(policyPath); + return policyPath; + } + catch (Exception ex) + { + Log.Error($"Policy-provided SettingsFolderLocation contains invalid characters: {policyPath}. Falling back to default location.", ex); + } + } + } // Fall back to existing logic return ConfigurationManager.Current.IsPortable diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md index 56e85d6b5d..af3427efbe 100644 --- a/Website/docs/system-wide-policies.md +++ b/Website/docs/system-wide-policies.md @@ -137,11 +137,15 @@ See [Location](./settings/settings.md#location) for more details. :::warning When configuring a custom settings folder location, ensure: +- The path **must be an absolute path** (e.g., `C:\\Path\\To\\Folder`, not `.\\RelativePath`) - The path uses double backslashes (`\\`) in JSON format +- The path does not contain invalid characters for the file system - The application has read/write permissions to the specified directory - The directory exists or the application has permissions to create it - All users have appropriate access to the folder if using a shared location +If the policy-provided path is invalid (not absolute or contains invalid characters), the application will log an error and fall back to the default location. + ::: ## Future Policies From 65e973245c5279bf8b68830adc75afb0a060df30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:13:45 +0000 Subject: [PATCH 04/25] Improve exception handling and documentation structure Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Settings/SettingsManager.cs | 8 ++++++-- Website/docs/system-wide-policies.md | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 1343bf2c81..f220b24587 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -92,14 +92,18 @@ public static string GetSettingsFolderLocation() // Validate that the path doesn't contain invalid characters try { - // This will throw if the path contains invalid characters + // This will throw ArgumentException or NotSupportedException if the path contains invalid characters _ = Path.GetFullPath(policyPath); return policyPath; } - catch (Exception ex) + catch (ArgumentException ex) { Log.Error($"Policy-provided SettingsFolderLocation contains invalid characters: {policyPath}. Falling back to default location.", ex); } + catch (NotSupportedException ex) + { + Log.Error($"Policy-provided SettingsFolderLocation format is not supported: {policyPath}. Falling back to default location.", ex); + } } } diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md index af3427efbe..c9126a5d51 100644 --- a/Website/docs/system-wide-policies.md +++ b/Website/docs/system-wide-policies.md @@ -32,7 +32,7 @@ The `config.json` file uses a simple JSON structure to define policy values. An ``` -**Example:** +**Example with available policies:** ```json { @@ -41,6 +41,8 @@ The `config.json` file uses a simple JSON structure to define policy values. An } ``` +For a complete list of available policies, see the [Available Policies](#available-policies) section below. + Property names generally follow the pattern `Section_SettingName` (see each setting's documentation). Ensure values use the correct JSON type (boolean, string, number, etc.). :::note From 114f86ce82bdeac838d65bfb6e846f19af996768 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:14:31 +0000 Subject: [PATCH 05/25] Add SecurityException handling and fix documentation formatting Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Settings/SettingsManager.cs | 6 +++++- Website/docs/system-wide-policies.md | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index f220b24587..6a8c399a9a 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -92,7 +92,7 @@ public static string GetSettingsFolderLocation() // Validate that the path doesn't contain invalid characters try { - // This will throw ArgumentException or NotSupportedException if the path contains invalid characters + // This will throw ArgumentException, NotSupportedException, or SecurityException if the path is invalid _ = Path.GetFullPath(policyPath); return policyPath; } @@ -104,6 +104,10 @@ public static string GetSettingsFolderLocation() { Log.Error($"Policy-provided SettingsFolderLocation format is not supported: {policyPath}. Falling back to default location.", ex); } + catch (System.Security.SecurityException ex) + { + Log.Error($"Insufficient permissions to access policy-provided SettingsFolderLocation: {policyPath}. Falling back to default location.", ex); + } } } diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md index c9126a5d51..be39de8f05 100644 --- a/Website/docs/system-wide-policies.md +++ b/Website/docs/system-wide-policies.md @@ -32,7 +32,7 @@ The `config.json` file uses a simple JSON structure to define policy values. An ``` -**Example with available policies:** +**Example:** ```json { From 92530c7e71484a91f8580b9bb9171a230ad95ae7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:15:20 +0000 Subject: [PATCH 06/25] Fix using statement and clarify path format documentation Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Source/NETworkManager.Settings/SettingsManager.cs | 3 ++- Website/docs/system-wide-policies.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 6a8c399a9a..343eac7ec2 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using System.Security; using System.Text.Json; using System.Text.Json.Serialization; using System.Xml.Serialization; @@ -104,7 +105,7 @@ public static string GetSettingsFolderLocation() { Log.Error($"Policy-provided SettingsFolderLocation format is not supported: {policyPath}. Falling back to default location.", ex); } - catch (System.Security.SecurityException ex) + catch (SecurityException ex) { Log.Error($"Insufficient permissions to access policy-provided SettingsFolderLocation: {policyPath}. Falling back to default location.", ex); } diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md index be39de8f05..87aa09f229 100644 --- a/Website/docs/system-wide-policies.md +++ b/Website/docs/system-wide-policies.md @@ -139,8 +139,8 @@ See [Location](./settings/settings.md#location) for more details. :::warning When configuring a custom settings folder location, ensure: -- The path **must be an absolute path** (e.g., `C:\\Path\\To\\Folder`, not `.\\RelativePath`) -- The path uses double backslashes (`\\`) in JSON format +- The path **must be an absolute path** (e.g., `C:\\Path\\To\\Folder` or `C:/Path/To/Folder`, not `.\\RelativePath`) +- For Windows paths, use either double backslashes (`\\`) or forward slashes (`/`) in JSON format (both `C:\\Path\\Folder` and `C:/Path/Folder` are valid) - The path does not contain invalid characters for the file system - The application has read/write permissions to the specified directory - The directory exists or the application has permissions to create it From 41633bf1066accc33231f47e72e12b78f692d218 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:16:04 +0000 Subject: [PATCH 07/25] Use consistent example path in documentation Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- Website/docs/system-wide-policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md index 87aa09f229..c8c2861010 100644 --- a/Website/docs/system-wide-policies.md +++ b/Website/docs/system-wide-policies.md @@ -37,7 +37,7 @@ The `config.json` file uses a simple JSON structure to define policy values. An ```json { "Update_CheckForUpdatesAtStartup": false, - "SettingsFolderLocation": "C:\\ProgramData\\NETworkManager\\Settings" + "SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings" } ``` From a58b7e091e6cbb75f4e2c31a87937b16c44388f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:35:06 +0000 Subject: [PATCH 08/25] Address review feedback: validate directory, update docs, add admin indicator Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> --- .../SettingsManager.cs | 13 ++++- .../ViewModels/SettingsSettingsViewModel.cs | 5 ++ .../Views/SettingsSettingsView.xaml | 15 ++++++ Website/docs/settings/settings.md | 20 +++++++- Website/docs/system-wide-policies.md | 50 ------------------- 5 files changed, 49 insertions(+), 54 deletions(-) diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 343eac7ec2..76961df19b 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -94,8 +94,17 @@ public static string GetSettingsFolderLocation() try { // This will throw ArgumentException, NotSupportedException, or SecurityException if the path is invalid - _ = Path.GetFullPath(policyPath); - return policyPath; + var fullPath = Path.GetFullPath(policyPath); + + // Check if the path is a directory (not a file) + if (File.Exists(fullPath)) + { + Log.Error($"Policy-provided SettingsFolderLocation is a file, not a directory: {policyPath}. Falling back to default location."); + } + else + { + return fullPath; + } } catch (ArgumentException ex) { diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs index 330f98e21e..aa41762eb5 100644 --- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs @@ -31,6 +31,11 @@ public string Location } } + /// + /// Gets whether the settings folder location is managed by system-wide policy. + /// + public bool IsLocationManagedByPolicy => !string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation); + private bool _isDailyBackupEnabled; public bool IsDailyBackupEnabled diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml index b51f7fe9e6..769491d08d 100644 --- a/Source/NETworkManager/Views/SettingsSettingsView.xaml +++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml @@ -7,12 +7,27 @@ xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" + xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters" mc:Ignorable="d" Loaded="UserControl_Loaded" d:DataContext="{d:DesignInstance viewModels:SettingsSettingsViewModel}"> + + + + + + + + + + + + + + + From 6cdf5259512d1acf33773890d696c61c87eaa21c Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 15 Feb 2026 23:18:44 +0100 Subject: [PATCH 11/25] Chore: Handle user settings location --- .../NETworkManager.Profiles/ProfileManager.cs | 6 +- .../LocalSettingsInfo.cs | 58 +++++++ .../LocalSettingsManager.cs | 123 +++++++++++++++ .../NETworkManager.Settings/SettingsInfo.cs | 10 ++ .../SettingsManager.cs | 143 +++++++++++------- Source/NETworkManager/App.xaml.cs | 31 ++-- 6 files changed, 298 insertions(+), 73 deletions(-) create mode 100644 Source/NETworkManager.Settings/LocalSettingsInfo.cs create mode 100644 Source/NETworkManager.Settings/LocalSettingsManager.cs diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index d9c0d33891..bbb62fa382 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -616,10 +616,10 @@ private static void Load(ProfileFileInfo profileFileInfo) { var loadedProfileUpdated = false; - Log.Info($"Load profile file: {profileFileInfo.Path}"); - if (File.Exists(profileFileInfo.Path)) { + Log.Info($"Loading profile file from: {profileFileInfo.Path}"); + // Encrypted profile file if (profileFileInfo.IsEncrypted) { @@ -734,6 +734,8 @@ private static void Load(ProfileFileInfo profileFileInfo) // Notify subscribers that profiles have been loaded/updated ProfilesUpdated(false); + + Log.Info("Profile file loaded successfully."); } /// diff --git a/Source/NETworkManager.Settings/LocalSettingsInfo.cs b/Source/NETworkManager.Settings/LocalSettingsInfo.cs new file mode 100644 index 0000000000..6c7d1a692f --- /dev/null +++ b/Source/NETworkManager.Settings/LocalSettingsInfo.cs @@ -0,0 +1,58 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; + +namespace NETworkManager.Settings; + +/// +/// Class contains local settings that are stored outside the main settings file. +/// These settings control where the main settings file is located. +/// +public class LocalSettingsInfo +{ + /// + /// Occurs when a property value changes. + /// + /// This event is typically used to notify subscribers that a property value has been updated. It + /// is commonly implemented in classes that support data binding or need to signal changes to property + /// values. + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Helper method to raise the event. + /// + /// Name of the property that changed. + private void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + SettingsChanged = true; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #region Variables + + [JsonIgnore] public bool SettingsChanged { get; set; } + + /// + /// Private field for the property." + /// + private string _settingsFolderLocation; + + /// + /// Location of the folder where the local settings file is stored. + /// This can be changed by the user to move the settings file to a different location. + /// + public string SettingsFolderLocation + { + get => _settingsFolderLocation; + set + { + if (_settingsFolderLocation == value) + return; + + _settingsFolderLocation = value; + OnPropertyChanged(); + } + } + #endregion +} diff --git a/Source/NETworkManager.Settings/LocalSettingsManager.cs b/Source/NETworkManager.Settings/LocalSettingsManager.cs new file mode 100644 index 0000000000..3826d35e86 --- /dev/null +++ b/Source/NETworkManager.Settings/LocalSettingsManager.cs @@ -0,0 +1,123 @@ +using log4net; +using Microsoft.Xaml.Behaviors.Media; +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NETworkManager.Settings; + +/// +/// Manages local application settings that are stored outside the main settings file. +/// This is used for settings that control where the main settings file is located. +/// +public static class LocalSettingsManager +{ + #region Variables + + /// + /// Logger for logging. + /// + private static readonly ILog Log = LogManager.GetLogger(typeof(LocalSettingsManager)); + + /// + /// Settings file name. + /// + private static string SettingsFileName => "Settings.json"; + + /// + /// Settings that are currently loaded. + /// + public static LocalSettingsInfo Current { get; private set; } + + /// + /// JSON serializer options for consistent serialization/deserialization. + /// + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + Converters = { new JsonStringEnumConverter() } + }; + #endregion + + #region Methods + + /// + /// Method to get the path of the settings folder. + /// + /// Path to the settings folder. + private static string GetSettingsFolderLocation() + { + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + AssemblyManager.Current.Name); + } + + /// + /// Method to get the settings file path + /// + /// Settings file path. + private static string GetSettingsFilePath() + { + return Path.Combine( + GetSettingsFolderLocation(), + SettingsFileName); + } + + /// + /// Initialize new settings () and save them (to a file). + /// + private static void Initialize() + { + Log.Info("Initializing new local settings."); + + Current = new LocalSettingsInfo(); + + Save(); + } + + /// + /// Method to load the settings from a file. + /// + public static void Load() + { + var filePath = GetSettingsFilePath(); + + if (File.Exists(filePath)) + { + Log.Info($"Loading local settings from: {filePath}"); + + var jsonString = File.ReadAllText(filePath); + Current = JsonSerializer.Deserialize(jsonString, JsonOptions); + + Log.Info("Local settings loaded successfully."); + + return; + } + + Initialize(); + } + + /// + /// Method to save the current settings to a file. + /// + public static void Save() + { + // Create the directory if it does not exist + Directory.CreateDirectory(GetSettingsFolderLocation()); + + // Serialize to file + var filePath = GetSettingsFilePath(); + + var jsonString = JsonSerializer.Serialize(Current, JsonOptions); + File.WriteAllText(filePath, jsonString); + + Log.Info($"Local settings saved to {filePath}"); + + // Reset change tracking + Current.SettingsChanged = false; + } + #endregion +} diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index 97834d7515..5e810c8ec3 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -19,6 +19,16 @@ namespace NETworkManager.Settings; +/// +/// Represents the application settings, user preferences, and configuration data for all supported features and +/// modules. Supports property change notification for data binding and persistence scenarios. +/// +/// The class provides a centralized container for storing and managing user-configurable +/// options, operational parameters, and history collections for various application modules, such as network tools, +/// remote access, and calculators. It implements the INotifyPropertyChanged interface to enable data binding and +/// automatic UI updates when settings change. Most properties raise the PropertyChanged event when modified, allowing +/// consumers to track changes and persist settings as needed. This class is typically used as the main settings model +/// in applications that require user customization and state management across sessions. public class SettingsInfo : INotifyPropertyChanged { /// diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 76961df19b..dcdd67ba11 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -9,6 +9,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Xml.Serialization; +using Windows.Gaming.Input.Custom; namespace NETworkManager.Settings; @@ -78,54 +79,85 @@ public static class SettingsManager /// Path to the settings folder. public static string GetSettingsFolderLocation() { - // Policy override takes precedence + // 1. Policy override takes precedence (for IT administrators) if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation)) { - var policyPath = PolicyManager.Current.SettingsFolderLocation; + var validatedPath = ValidateSettingsFolderPath( + PolicyManager.Current.SettingsFolderLocation, + "Policy-provided", + "next priority"); - // Validate that the policy-provided path is rooted (absolute) - if (!Path.IsPathRooted(policyPath)) - { - Log.Error($"Policy-provided SettingsFolderLocation is not an absolute path: {policyPath}. Falling back to default location."); - } - else + if (validatedPath != null) + return validatedPath; + } + + // 2. Custom user-configured path (not available in portable mode) + if (!ConfigurationManager.Current.IsPortable && + !string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.SettingsFolderLocation)) + { + var validatedPath = ValidateSettingsFolderPath( + LocalSettingsManager.Current.SettingsFolderLocation, + "Custom", + "default location"); + + if (validatedPath != null) + return validatedPath; + } + + // 3. Fall back to portable or default location + if (ConfigurationManager.Current.IsPortable) + return Path.Combine(AssemblyManager.Current.Location, SettingsFolderName); + else + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + AssemblyManager.Current.Name, SettingsFolderName); + } + + /// + /// Validates a settings folder path for correctness and accessibility. + /// + /// The path to validate. + /// Description of the path source for logging (e.g., "Policy-provided", "Custom"). + /// Message describing what happens on validation failure (e.g., "next priority", "default location"). + /// The validated full path if valid; otherwise, null. + private static string ValidateSettingsFolderPath(string path, string pathSource, string fallbackMessage) + { + // Validate that the path is rooted (absolute) + if (!Path.IsPathRooted(path)) + { + Log.Error($"{pathSource} SettingsFolderLocation is not an absolute path: {path}. Falling back to {fallbackMessage}."); + return null; + } + + // Validate that the path doesn't contain invalid characters + try + { + // This will throw ArgumentException, NotSupportedException, or SecurityException if the path is invalid + var fullPath = Path.GetFullPath(path); + + // Check if the path is a directory (not a file) + if (File.Exists(fullPath)) { - // Validate that the path doesn't contain invalid characters - try - { - // This will throw ArgumentException, NotSupportedException, or SecurityException if the path is invalid - var fullPath = Path.GetFullPath(policyPath); - - // Check if the path is a directory (not a file) - if (File.Exists(fullPath)) - { - Log.Error($"Policy-provided SettingsFolderLocation is a file, not a directory: {policyPath}. Falling back to default location."); - } - else - { - return fullPath; - } - } - catch (ArgumentException ex) - { - Log.Error($"Policy-provided SettingsFolderLocation contains invalid characters: {policyPath}. Falling back to default location.", ex); - } - catch (NotSupportedException ex) - { - Log.Error($"Policy-provided SettingsFolderLocation format is not supported: {policyPath}. Falling back to default location.", ex); - } - catch (SecurityException ex) - { - Log.Error($"Insufficient permissions to access policy-provided SettingsFolderLocation: {policyPath}. Falling back to default location.", ex); - } + Log.Error($"{pathSource} SettingsFolderLocation is a file, not a directory: {path}. Falling back to {fallbackMessage}."); + return null; } - } - // Fall back to existing logic - return ConfigurationManager.Current.IsPortable - ? Path.Combine(AssemblyManager.Current.Location, SettingsFolderName) - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - AssemblyManager.Current.Name, SettingsFolderName); + return fullPath; + } + catch (ArgumentException ex) + { + Log.Error($"{pathSource} SettingsFolderLocation contains invalid characters: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } + catch (NotSupportedException ex) + { + Log.Error($"{pathSource} SettingsFolderLocation format is not supported: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } + catch (SecurityException ex) + { + Log.Error($"Insufficient permissions to access {pathSource} SettingsFolderLocation: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } } /// @@ -174,7 +206,6 @@ private static string GetLegacySettingsFilePath() { return Path.Combine(GetSettingsFolderLocation(), GetLegacySettingsFileName()); } - #endregion #region Initialize, load and save @@ -184,6 +215,8 @@ private static string GetLegacySettingsFilePath() /// public static void Initialize() { + Log.Info("Initializing new settings."); + Current = new SettingsInfo { Version = AssemblyManager.Current.Version.ToString() @@ -203,8 +236,12 @@ public static void Load() // Check if JSON file exists if (File.Exists(filePath)) { + Log.Info($"Loading settings from: {filePath}"); + Current = DeserializeFromFile(filePath); + Log.Info("Settings loaded successfully."); + Current.SettingsChanged = false; return; @@ -280,24 +317,18 @@ public static void Save() // Create backup before modifying CreateDailyBackupIfNeeded(); - // Serialize the settings to a file - SerializeToFile(GetSettingsFilePath()); - - // Set the setting changed to false after saving them as file... - Current.SettingsChanged = false; - } + // Serialize to file + var filePath = GetSettingsFilePath(); - /// - /// Method to serialize the settings to a JSON file. - /// - /// Path to the settings file. - private static void SerializeToFile(string filePath) - { var jsonString = JsonSerializer.Serialize(Current, JsonOptions); File.WriteAllText(filePath, jsonString); - } + Log.Info($"Settings saved to {filePath}"); + + // Reset change tracking + Current.SettingsChanged = false; + } #endregion #region Backup diff --git a/Source/NETworkManager/App.xaml.cs b/Source/NETworkManager/App.xaml.cs index 42d0f8503e..1070bc951b 100644 --- a/Source/NETworkManager/App.xaml.cs +++ b/Source/NETworkManager/App.xaml.cs @@ -21,12 +21,13 @@ namespace NETworkManager; * 2) Detect current configuration * 3) Get assembly info * 4) Load system-wide policies - * 5) Load settings - * 6) Load localization / language + * 5) Load local settings + * 6) Load settings + * 7) Load localization / language * * Class: MainWindow - * 7) Load appearance - * 8) Load profiles + * 8) Load appearance + * 9) Load profiles */ public partial class App @@ -85,11 +86,12 @@ by BornToBeRoot // Load system-wide policies PolicyManager.Load(); + // Load (or initialize) local settings + LocalSettingsManager.Load(); + // Load (or initialize) settings try { - Log.Info("Application settings are being loaded..."); - if (CommandLineManager.Current.ResetSettings) SettingsManager.Initialize(); else @@ -112,21 +114,13 @@ by BornToBeRoot var settingsVersion = Version.Parse(SettingsManager.Current.Version); if (settingsVersion < AssemblyManager.Current.Version) - { - Log.Info( - $"Application settings are on version {settingsVersion} and will be upgraded to {AssemblyManager.Current.Version}"); - SettingsManager.Upgrade(settingsVersion, AssemblyManager.Current.Version); - - Log.Info($"Application settings upgraded to version {AssemblyManager.Current.Version}"); - } else - { Log.Info($"Application settings are already on version {AssemblyManager.Current.Version}."); - } // Initialize localization var localizationManager = LocalizationManager.GetInstance(SettingsManager.Current.Localization_CultureCode); + Strings.Culture = localizationManager.Culture; Log.Info( @@ -305,6 +299,13 @@ private void Application_Exit(object sender, ExitEventArgs e) /// file is encrypted and not unlocked, profile data will not be saved and a warning is logged. private void Save() { + // Save local settings if they have changed + if (LocalSettingsManager.Current.SettingsChanged) + { + Log.Info("Save local settings..."); + LocalSettingsManager.Save(); + } + // Save settings if they have changed if (SettingsManager.Current.SettingsChanged) { From 26410ca145ec158d8ced2cfece5b571bdb458130 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:05:45 +0100 Subject: [PATCH 12/25] Feature: UI change settings --- .../Resources/Strings.Designer.cs | 27 ++++-- .../Resources/Strings.resx | 3 + .../ViewModels/SettingsSettingsViewModel.cs | 20 +++++ .../Views/SettingsSettingsView.xaml | 88 ++++++++++++++++--- .../Views/SettingsSettingsView.xaml.cs | 17 ++++ 5 files changed, 132 insertions(+), 23 deletions(-) diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 8fa04498df..d209febb73 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -8835,6 +8835,15 @@ public static string RestartTheApplicationToChangeTheLanguage { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die Restore default location ähnelt. + /// + public static string RestoreDefaultLocation { + get { + return ResourceManager.GetString("RestoreDefaultLocation", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Restore defaults ähnelt. /// @@ -9213,6 +9222,15 @@ public static string SetMasterPasswordDots { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die This setting is managed by your administrator. ähnelt. + /// + public static string SettingManagedByAdministrator { + get { + return ResourceManager.GetString("SettingManagedByAdministrator", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Settings ähnelt. /// @@ -9271,15 +9289,6 @@ public static string SettingsHaveBeenReset { } } - /// - /// Sucht eine lokalisierte Zeichenfolge, die This setting is managed by your administrator. ähnelt. - /// - public static string SettingManagedByAdministrator { - get { - return ResourceManager.GetString("SettingManagedByAdministrator", resourceCulture); - } - } - /// /// Sucht eine lokalisierte Zeichenfolge, die Appearance ähnelt. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index e4f0fef96e..5db427d08c 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3963,4 +3963,7 @@ If you click Cancel, the profile file will remain unencrypted. This setting is managed by your administrator. + + Restore default location + \ No newline at end of file diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs index aa41762eb5..71249d8324 100644 --- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs @@ -3,6 +3,7 @@ using NETworkManager.Utilities; using System; using System.Diagnostics; +using System.IO; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -113,6 +114,25 @@ private void ResetSettingsAction() #endregion #region Methods + public ICommand BrowseLocationFolderCommand => new RelayCommand(p => BrowseLocationFolderAction()); + + private void BrowseLocationFolderAction() + { + using var dialog = new System.Windows.Forms.FolderBrowserDialog(); + + if (Directory.Exists(Location)) + dialog.SelectedPath = Location; + + var dialogResult = dialog.ShowDialog(); + + if (dialogResult == System.Windows.Forms.DialogResult.OK) + Location = dialog.SelectedPath; + } + + public void SetLocationPathFromDragDrop(string path) + { + Location = path; + } private async Task ResetSettings() { diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml index 8d20d869d5..c65b7a4534 100644 --- a/Source/NETworkManager/Views/SettingsSettingsView.xaml +++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml @@ -8,16 +8,34 @@ xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters" + xmlns:validators="clr-namespace:NETworkManager.Validators;assembly=NETworkManager.Validators" mc:Ignorable="d" Loaded="UserControl_Loaded" d:DataContext="{d:DesignInstance viewModels:SettingsSettingsViewModel}"> + + - - + + + + + + + + + + @@ -25,21 +43,61 @@ + Visibility="{Binding IsLocationManagedByPolicy, Converter={StaticResource BooleanReverseToVisibilityCollapsedConverter}}"> + + + + - + - - + + Date: Mon, 16 Feb 2026 02:13:10 +0100 Subject: [PATCH 13/25] Feature: UI --- .../Resources/Strings.Designer.cs | 29 +++++ .../Resources/Strings.resx | 11 ++ .../LocalSettingsManager.cs | 3 + .../SettingsManager.cs | 25 +++- .../ViewModels/SettingsSettingsViewModel.cs | 114 +++++++++++++++++- .../Views/SettingsSettingsView.xaml | 72 +++++++++-- 6 files changed, 239 insertions(+), 15 deletions(-) diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index d209febb73..2b2889dfbd 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -8835,6 +8835,15 @@ public static string RestartTheApplicationToChangeTheLanguage { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die Restore ähnelt. + /// + public static string Restore { + get { + return ResourceManager.GetString("Restore", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Restore default location ähnelt. /// @@ -8844,6 +8853,26 @@ public static string RestoreDefaultLocation { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die Restore default location? ähnelt. + /// + public static string RestoreDefaultLocationQuestion { + get { + return ResourceManager.GetString("RestoreDefaultLocationQuestion", resourceCulture); + } + } + + /// + /// Sucht eine lokalisierte Zeichenfolge, die The default path is restored and the application is restarted afterwards. + /// + ///You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten. ähnelt. + /// + public static string RestoreDefaultLocationSettingsMessage { + get { + return ResourceManager.GetString("RestoreDefaultLocationSettingsMessage", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Restore defaults ähnelt. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 5db427d08c..d2d3adada4 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3966,4 +3966,15 @@ If you click Cancel, the profile file will remain unencrypted. Restore default location + + Restore + + + Restore default location? + + + The default path is restored and the application is restarted afterwards. + +You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten. + \ No newline at end of file diff --git a/Source/NETworkManager.Settings/LocalSettingsManager.cs b/Source/NETworkManager.Settings/LocalSettingsManager.cs index 3826d35e86..c8fd3cc983 100644 --- a/Source/NETworkManager.Settings/LocalSettingsManager.cs +++ b/Source/NETworkManager.Settings/LocalSettingsManager.cs @@ -94,6 +94,9 @@ public static void Load() Log.Info("Local settings loaded successfully."); + // Reset change tracking + Current.SettingsChanged = false; + return; } diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index dcdd67ba11..a8d812fa4f 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -106,10 +106,28 @@ public static string GetSettingsFolderLocation() // 3. Fall back to portable or default location if (ConfigurationManager.Current.IsPortable) - return Path.Combine(AssemblyManager.Current.Location, SettingsFolderName); + return GetPortableSettingsFolderLocation(); else - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - AssemblyManager.Current.Name, SettingsFolderName); + return GetDefaultSettingsFolderLocation(); + } + + /// + /// Method to get the default settings folder location in the user's Documents directory. + /// + /// Path to the default settings folder location. + public static string GetDefaultSettingsFolderLocation() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + AssemblyManager.Current.Name, SettingsFolderName); + } + + /// + /// Method to get the portable settings folder location (in the same directory as the application). + /// + /// Path to the portable settings folder location. + public static string GetPortableSettingsFolderLocation() + { + return Path.Combine(AssemblyManager.Current.Location, SettingsFolderName); } /// @@ -242,6 +260,7 @@ public static void Load() Log.Info("Settings loaded successfully."); + // Reset change tracking Current.SettingsChanged = false; return; diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs index 71249d8324..7f6ae17c21 100644 --- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs @@ -15,10 +15,19 @@ public class SettingsSettingsViewModel : ViewModelBase #region Variables public Action CloseAction { get; set; } + /// + /// Indicates whether the settings are currently being loaded to prevent triggering change events during initialization. + /// private readonly bool _isLoading; + /// + /// Private field of property. + /// private string _location; + /// + /// Gets or sets the file system path to the settings location. + /// public string Location { get => _location; @@ -27,18 +36,69 @@ public string Location if (value == _location) return; + if (!_isLoading) + IsLocationChanged = !string.Equals(value, SettingsManager.GetSettingsFolderLocation(), StringComparison.OrdinalIgnoreCase); + _location = value; OnPropertyChanged(); } } /// - /// Gets whether the settings folder location is managed by system-wide policy. + /// Indicates whether the settings location is managed by a system-wide policy. /// public bool IsLocationManagedByPolicy => !string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation); + /// + /// Private field of property. + /// + private bool _isLocationChanged; + + /// + /// Gets or sets a value indicating whether the location has changed. + /// + public bool IsLocationChanged + { + get => _isLocationChanged; + set + { + if (value == _isLocationChanged) + return; + + _isLocationChanged = value; + OnPropertyChanged(); + } + } + + /// + /// Private field of property. + /// + public bool _isDefaultLocation; + + /// + /// Indicates whether the current location is the default settings folder location. + /// + public bool IsDefaultLocation + { + get => _isDefaultLocation; + set + { + if (value == _isDefaultLocation) + return; + + _isDefaultLocation = value; + OnPropertyChanged(); + } + } + + /// + /// Private field of property. + /// private bool _isDailyBackupEnabled; + /// + /// Gets or sets a value indicating whether daily backups are enabled. + /// public bool IsDailyBackupEnabled { get => _isDailyBackupEnabled; @@ -55,8 +115,14 @@ public bool IsDailyBackupEnabled } } + /// + /// Private field of property. + /// private int _maximumNumberOfBackups; + /// + /// Gets or sets the maximum number of backups to keep. + /// public int MaximumNumberOfBackups { get => _maximumNumberOfBackups; @@ -89,6 +155,11 @@ public SettingsSettingsViewModel() private void LoadSettings() { Location = SettingsManager.GetSettingsFolderLocation(); + IsDefaultLocation = string.Equals(Location, SettingsManager.GetDefaultSettingsFolderLocation(), StringComparison.OrdinalIgnoreCase); + + Debug.WriteLine(Location); + Debug.WriteLine(SettingsManager.GetDefaultSettingsFolderLocation()); + IsDailyBackupEnabled = SettingsManager.Current.Settings_IsDailyBackupEnabled; MaximumNumberOfBackups = SettingsManager.Current.Settings_MaximumNumberOfBackups; } @@ -119,7 +190,7 @@ private void ResetSettingsAction() private void BrowseLocationFolderAction() { using var dialog = new System.Windows.Forms.FolderBrowserDialog(); - + if (Directory.Exists(Location)) dialog.SelectedPath = Location; @@ -134,6 +205,45 @@ public void SetLocationPathFromDragDrop(string path) Location = path; } + public ICommand ChangeLocationCommand => new RelayCommand(_ => ChangeLocationAction()); + + private async Task ChangeLocationAction() + { + /* + var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, + Strings.ChangeSettingsLocationQuestion, + Strings.SettingsLocationWillBeChangedAndApplicationWillBeRestartedMessage, + ChildWindowIcon.Question, + Strings.Apply); + + LocalSettingsManager.Save(); + + // Restart the application + (Application.Current.MainWindow as MainWindow)?.RestartApplication(); + */ + } + + public ICommand RestoreDefaultLocationCommand => new RelayCommand(_ => RestoreDefaultLocationActionAsync().ConfigureAwait(false)); + + private async Task RestoreDefaultLocationActionAsync() + { + var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, + Strings.RestoreDefaultLocationQuestion, + string.Format(Strings.RestoreDefaultLocationSettingsMessage, SettingsManager.GetSettingsFolderLocation(),SettingsManager.GetDefaultSettingsFolderLocation()), + ChildWindowIcon.Question, + Strings.Restore); + + if (!result) + return; + + LocalSettingsManager.Current.SettingsFolderLocation = null; + + LocalSettingsManager.Save(); + + // Restart the application + (Application.Current.MainWindow as MainWindow)?.RestartApplication(); + } + private async Task ResetSettings() { var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml index c65b7a4534..d3a3134561 100644 --- a/Source/NETworkManager/Views/SettingsSettingsView.xaml +++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml @@ -9,6 +9,7 @@ xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters" xmlns:validators="clr-namespace:NETworkManager.Validators;assembly=NETworkManager.Validators" + xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings" mc:Ignorable="d" Loaded="UserControl_Loaded" d:DataContext="{d:DesignInstance viewModels:SettingsSettingsViewModel}"> @@ -19,18 +20,29 @@ + + + + - + @@ -42,8 +54,19 @@ + Orientation="Horizontal"> + + + - - + Date: Mon, 16 Feb 2026 02:29:59 +0100 Subject: [PATCH 14/25] Feature: UI --- .../Resources/Strings.Designer.cs | 20 ++++++++++++ .../Resources/Strings.resx | 8 +++++ .../ViewModels/SettingsSettingsViewModel.cs | 31 +++++++++++++------ .../Views/SettingsSettingsView.xaml | 7 +++-- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 2b2889dfbd..6510d5f2b3 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -1455,6 +1455,26 @@ public static string Change { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die Change location? ähnelt. + /// + public static string ChangeLocationQuestion { + get { + return ResourceManager.GetString("ChangeLocationQuestion", resourceCulture); + } + } + + /// + /// Sucht eine lokalisierte Zeichenfolge, die The location is changed and the application is restarted afterwards. + /// + ///You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten. ähnelt. + /// + public static string ChangeLocationSettingsMessage { + get { + return ResourceManager.GetString("ChangeLocationSettingsMessage", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Changelog ähnelt. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index d2d3adada4..db5f80a6fa 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3975,6 +3975,14 @@ If you click Cancel, the profile file will remain unencrypted. The default path is restored and the application is restarted afterwards. +You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten. + + + Change location? + + + The location is changed and the application is restarted afterwards. + You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten. \ No newline at end of file diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs index 7f6ae17c21..533da2cc0e 100644 --- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs @@ -205,22 +205,29 @@ public void SetLocationPathFromDragDrop(string path) Location = path; } - public ICommand ChangeLocationCommand => new RelayCommand(_ => ChangeLocationAction()); + public ICommand ChangeLocationCommand => new RelayCommand(_ => ChangeLocationAction().ConfigureAwait(false)); private async Task ChangeLocationAction() { - /* - var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, - Strings.ChangeSettingsLocationQuestion, - Strings.SettingsLocationWillBeChangedAndApplicationWillBeRestartedMessage, - ChildWindowIcon.Question, - Strings.Apply); + var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, + Strings.ChangeLocationQuestion, + string.Format(Strings.ChangeLocationSettingsMessage, SettingsManager.GetSettingsFolderLocation(), Location), + ChildWindowIcon.Question, + Strings.Change); + + if (!result) + return; + + // Save settings at the current location before changing it to prevent + // unintended saves to the new location (e.g., triggered by background timer or the app close & restart). + SettingsManager.Save(); + // Set new location + LocalSettingsManager.Current.SettingsFolderLocation = Location; LocalSettingsManager.Save(); // Restart the application (Application.Current.MainWindow as MainWindow)?.RestartApplication(); - */ } public ICommand RestoreDefaultLocationCommand => new RelayCommand(_ => RestoreDefaultLocationActionAsync().ConfigureAwait(false)); @@ -229,15 +236,19 @@ private async Task RestoreDefaultLocationActionAsync() { var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, Strings.RestoreDefaultLocationQuestion, - string.Format(Strings.RestoreDefaultLocationSettingsMessage, SettingsManager.GetSettingsFolderLocation(),SettingsManager.GetDefaultSettingsFolderLocation()), + string.Format(Strings.RestoreDefaultLocationSettingsMessage, SettingsManager.GetSettingsFolderLocation(), SettingsManager.GetDefaultSettingsFolderLocation()), ChildWindowIcon.Question, Strings.Restore); if (!result) return; - LocalSettingsManager.Current.SettingsFolderLocation = null; + // Save settings at the current location before changing it to prevent + // unintended saves to the new location (e.g., triggered by background timer or the app close & restart). + SettingsManager.Save(); + // Clear custom location to revert to default + LocalSettingsManager.Current.SettingsFolderLocation = null; LocalSettingsManager.Save(); // Restart the application diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml index d3a3134561..5b2ead09f5 100644 --- a/Source/NETworkManager/Views/SettingsSettingsView.xaml +++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml @@ -68,7 +68,10 @@ + /// The value to validate as a directory path. May include environment variable references. Can be a string or an + /// object convertible to a string. + /// The culture-specific information relevant to the validation process. This parameter is not used in this + /// implementation. + /// A ValidationResult that indicates whether the value is a valid directory path. Returns + /// ValidationResult.ValidResult if the value is valid; otherwise, returns a ValidationResult with an error message. public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - var path = Environment.ExpandEnvironmentVariables((string)value); + var path = $"{value}"; - return new Regex(RegexHelper.FilePathRegex, RegexOptions.IgnoreCase).IsMatch(path) + return RegexHelper.DirectoryPathWithEnvironmentVariablesRegex().IsMatch(path) || RegexHelper.UncPathRegex().IsMatch(path) ? ValidationResult.ValidResult - : new ValidationResult(false, Strings.EnterValidFilePath); + : new ValidationResult(false, Strings.EnterValidFolderPath); } -} \ No newline at end of file +} diff --git a/Source/NETworkManager/NETworkManager.csproj b/Source/NETworkManager/NETworkManager.csproj index 3dee3d3535..a61640f1f5 100644 --- a/Source/NETworkManager/NETworkManager.csproj +++ b/Source/NETworkManager/NETworkManager.csproj @@ -8,10 +8,6 @@ win-x64 x64 false - sdk true true @@ -74,8 +70,7 @@ - - + diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs index 533da2cc0e..fc635e8570 100644 --- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs @@ -13,6 +13,9 @@ namespace NETworkManager.ViewModels; public class SettingsSettingsViewModel : ViewModelBase { #region Variables + /// + /// Gets or sets the action to execute when the associated object is closed. + /// public Action CloseAction { get; set; } /// @@ -143,6 +146,9 @@ public int MaximumNumberOfBackups #region Constructor, LoadSettings + /// + /// Initializes a new instance of the class and loads the current settings. + /// public SettingsSettingsViewModel() { _isLoading = true; @@ -152,14 +158,13 @@ public SettingsSettingsViewModel() _isLoading = false; } + /// + /// Loads the application settings from the current settings folder location. + /// private void LoadSettings() { Location = SettingsManager.GetSettingsFolderLocation(); IsDefaultLocation = string.Equals(Location, SettingsManager.GetDefaultSettingsFolderLocation(), StringComparison.OrdinalIgnoreCase); - - Debug.WriteLine(Location); - Debug.WriteLine(SettingsManager.GetDefaultSettingsFolderLocation()); - IsDailyBackupEnabled = SettingsManager.Current.Settings_IsDailyBackupEnabled; MaximumNumberOfBackups = SettingsManager.Current.Settings_MaximumNumberOfBackups; } @@ -168,15 +173,27 @@ private void LoadSettings() #region ICommands & Actions + /// + /// Gets the command that opens a location when executed. + /// public ICommand OpenLocationCommand => new RelayCommand(_ => OpenLocationAction()); + /// + /// Opens the settings folder location in Windows Explorer. + /// private static void OpenLocationAction() { Process.Start("explorer.exe", SettingsManager.GetSettingsFolderLocation()); } + /// + /// Gets the command that resets the application settings to their default values. + /// public ICommand ResetSettingsCommand => new RelayCommand(_ => ResetSettingsAction()); + /// + /// Resets the application settings to their default values. + /// private void ResetSettingsAction() { ResetSettings().ConfigureAwait(false); @@ -185,8 +202,18 @@ private void ResetSettingsAction() #endregion #region Methods + /// + /// Gets the command that opens the location folder selection dialog. + /// public ICommand BrowseLocationFolderCommand => new RelayCommand(p => BrowseLocationFolderAction()); + /// + /// Opens a dialog that allows the user to select a folder location and updates the Location property with the + /// selected path if the user confirms the selection. + /// + /// If the Location property is set to a valid directory path, it is pre-selected in the dialog. + /// This method does not return a value and is intended for use in a user interface context where folder selection + /// is required. private void BrowseLocationFolderAction() { using var dialog = new System.Windows.Forms.FolderBrowserDialog(); @@ -200,13 +227,28 @@ private void BrowseLocationFolderAction() Location = dialog.SelectedPath; } + /// + /// Sets the location path based on the provided drag-and-drop input. + /// + /// The path to set as the location. This value cannot be null or empty. public void SetLocationPathFromDragDrop(string path) { Location = path; } + /// + /// Gets the command that initiates the action to change the location. + /// public ICommand ChangeLocationCommand => new RelayCommand(_ => ChangeLocationAction().ConfigureAwait(false)); + /// + /// Prompts the user to confirm and then changes the location of the application's settings folder. + /// + /// This method displays a confirmation dialog to the user before changing the settings folder + /// location. If the user confirms, it saves the current settings, updates the settings folder location, and + /// restarts the application to apply the changes. No action is taken if the user cancels the confirmation + /// dialog. + /// A task that represents the asynchronous operation. private async Task ChangeLocationAction() { var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, @@ -230,8 +272,19 @@ private async Task ChangeLocationAction() (Application.Current.MainWindow as MainWindow)?.RestartApplication(); } + /// + /// Gets the command that restores the default location settings asynchronously. + /// public ICommand RestoreDefaultLocationCommand => new RelayCommand(_ => RestoreDefaultLocationActionAsync().ConfigureAwait(false)); + /// + /// Restores the application's settings folder location to the default path after obtaining user confirmation. + /// + /// This method prompts the user to confirm the restoration of the default settings location. If + /// the user confirms, it saves the current settings, clears any custom location, and restarts the application to + /// apply the changes. Use this method when you want to revert to the default settings folder and ensure all changes + /// are properly saved and applied. + /// A task that represents the asynchronous operation. private async Task RestoreDefaultLocationActionAsync() { var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, @@ -255,6 +308,13 @@ private async Task RestoreDefaultLocationActionAsync() (Application.Current.MainWindow as MainWindow)?.RestartApplication(); } + /// + /// Resets the application settings to their default values and restarts the application after user confirmation. + /// + /// Displays a confirmation dialog to the user before proceeding. If the user confirms, the + /// settings are reinitialized to their defaults and the application is restarted. No action is taken if the user + /// cancels the confirmation dialog. + /// A task that represents the asynchronous operation. private async Task ResetSettings() { var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml index 5b2ead09f5..d1db5b42be 100644 --- a/Source/NETworkManager/Views/SettingsSettingsView.xaml +++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml @@ -38,11 +38,11 @@ - + @@ -67,11 +67,19 @@ -