diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 40ef350d30..f7699adf75 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -1464,17 +1464,6 @@ public static string ChangeLocationQuestion { } } - /// - /// 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. /// @@ -1502,6 +1491,28 @@ public static string ChangeMasterPasswordDots { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die The profiles location is changed and the application is restarted afterwards. + /// + ///You can copy your profile files from “{0}” to “{1}” to migrate your existing profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten. ähnelt. + /// + public static string ChangeProfilesLocationMessage { + get { + return ResourceManager.GetString("ChangeProfilesLocationMessage", resourceCulture); + } + } + + /// + /// Sucht eine lokalisierte Zeichenfolge, die The settings 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 ChangeSettingsLocationMessage { + get { + return ResourceManager.GetString("ChangeSettingsLocationMessage", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Channel ähnelt. /// @@ -8892,13 +8903,13 @@ public static string RestoreDefaultLocationQuestion { } /// - /// Sucht eine lokalisierte Zeichenfolge, die The default path is restored and the application is restarted afterwards. + /// Sucht eine lokalisierte Zeichenfolge, die The default profiles location 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. + ///You can copy your profile files from “{0}” to “{1}” to migrate your existing profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten. ähnelt. /// - public static string RestoreDefaultLocationSettingsMessage { + public static string RestoreDefaultProfilesLocationMessage { get { - return ResourceManager.GetString("RestoreDefaultLocationSettingsMessage", resourceCulture); + return ResourceManager.GetString("RestoreDefaultProfilesLocationMessage", resourceCulture); } } @@ -8911,6 +8922,17 @@ public static string RestoreDefaults { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die The default settings location 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 RestoreDefaultSettingsLocationMessage { + get { + return ResourceManager.GetString("RestoreDefaultSettingsLocationMessage", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Result ähnelt. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index ad2eb07c93..7c2471b85e 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3972,20 +3972,30 @@ If you click Cancel, the profile file will remain unencrypted. Restore default location? - - The default path is restored and the application is restarted afterwards. + + The default settings location 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. + + The settings 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. Enter a valid folder path! + + The profiles location is changed and the application is restarted afterwards. + +You can copy your profile files from “{0}” to “{1}” to migrate your existing profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten. + + + The default profiles location is restored and the application is restarted afterwards. + +You can copy your profile files from “{0}” to “{1}” to migrate your existing profiles, if necessary. The application must be closed for this to prevent the profiles from being overwritten. + \ No newline at end of file diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index bbb62fa382..c32ae88977 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -197,14 +197,65 @@ private static void ProfilesUpdated(bool profilesChanged = true) /// /// Method to get the path of the profiles folder. + /// Priority: + /// 1. Policy override (for IT administrators) + /// 2. Custom user-configured path (not available in portable mode) + /// 3. Portable (same directory as the application) or default location (Documents folder) /// /// Path to the profiles folder. public static string GetProfilesFolderLocation() { - return ConfigurationManager.Current.IsPortable - ? Path.Combine(AssemblyManager.Current.Location, ProfilesFolderName) - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - AssemblyManager.Current.Name, ProfilesFolderName); + // 1. Policy override takes precedence (for IT administrators) + if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.Profiles_FolderLocation)) + { + var validatedPath = DirectoryHelper.ValidateFolderPath( + PolicyManager.Current.Profiles_FolderLocation, + "Policy-provided", + nameof(PolicyInfo.Profiles_FolderLocation), + "next priority"); + + if (validatedPath != null) + return validatedPath; + } + + // 2. Custom user-configured path (not available in portable mode) + if (!ConfigurationManager.Current.IsPortable && + !string.IsNullOrWhiteSpace(SettingsManager.Current?.Profiles_FolderLocation)) + { + var validatedPath = DirectoryHelper.ValidateFolderPath( + SettingsManager.Current.Profiles_FolderLocation, + "Custom", + nameof(SettingsInfo.Profiles_FolderLocation), + "default location"); + + if (validatedPath != null) + return validatedPath; + } + + // 3. Fall back to portable or default location + if (ConfigurationManager.Current.IsPortable) + return GetPortableProfilesFolderLocation(); + else + return GetDefaultProfilesFolderLocation(); + } + + /// + /// Method to get the default profiles folder location in the user's Documents directory. + /// + /// Path to the default profiles folder location. + public static string GetDefaultProfilesFolderLocation() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + AssemblyManager.Current.Name, ProfilesFolderName); + } + + /// + /// Method to get the portable profiles folder location (in the same directory as the application). + /// + /// Path to the portable profiles folder location. + public static string GetPortableProfilesFolderLocation() + { + return Path.Combine(AssemblyManager.Current.Location, ProfilesFolderName); } /// diff --git a/Source/NETworkManager.Settings/LocalSettingsInfo.cs b/Source/NETworkManager.Settings/LocalSettingsInfo.cs index 6c7d1a692f..79c02a1937 100644 --- a/Source/NETworkManager.Settings/LocalSettingsInfo.cs +++ b/Source/NETworkManager.Settings/LocalSettingsInfo.cs @@ -34,23 +34,23 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null) [JsonIgnore] public bool SettingsChanged { get; set; } /// - /// Private field for the property." + /// Private field for the property. /// - private string _settingsFolderLocation; + private string _settings_FolderLocation; /// /// 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 + public string Settings_FolderLocation { - get => _settingsFolderLocation; + get => _settings_FolderLocation; set { - if (_settingsFolderLocation == value) + if (_settings_FolderLocation == value) return; - _settingsFolderLocation = value; + _settings_FolderLocation = value; OnPropertyChanged(); } } diff --git a/Source/NETworkManager.Settings/PolicyInfo.cs b/Source/NETworkManager.Settings/PolicyInfo.cs index 2f17ee8abf..811360e765 100644 --- a/Source/NETworkManager.Settings/PolicyInfo.cs +++ b/Source/NETworkManager.Settings/PolicyInfo.cs @@ -11,6 +11,9 @@ public class PolicyInfo [JsonPropertyName("Update_CheckForUpdatesAtStartup")] public bool? Update_CheckForUpdatesAtStartup { get; set; } - [JsonPropertyName("SettingsFolderLocation")] - public string? SettingsFolderLocation { get; set; } + [JsonPropertyName("Profiles_FolderLocation")] + public string? Profiles_FolderLocation { get; set; } + + [JsonPropertyName("Settings_FolderLocation")] + public string? Settings_FolderLocation { get; set; } } diff --git a/Source/NETworkManager.Settings/PolicyManager.cs b/Source/NETworkManager.Settings/PolicyManager.cs index ddbdfd3d38..e7708db5fa 100644 --- a/Source/NETworkManager.Settings/PolicyManager.cs +++ b/Source/NETworkManager.Settings/PolicyManager.cs @@ -84,7 +84,8 @@ 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"}"); + Log.Info($"System-wide policy - Profiles_FolderLocation: {Current.Profiles_FolderLocation ?? "Not set"}"); + Log.Info($"System-wide policy - Settings_FolderLocation: {Current.Settings_FolderLocation ?? "Not set"}"); } } catch (Exception ex) diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index 5e810c8ec3..01f641b0b4 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -576,7 +576,22 @@ public bool Experimental_EnableExperimentalFeatures } } - // Profiles + // Profiles + private string _profiles_FolderLocation; + + public string Profiles_FolderLocation + { + get => _profiles_FolderLocation; + set + { + if (value == _profiles_FolderLocation) + return; + + _profiles_FolderLocation = value; + OnPropertyChanged(); + } + } + private string _profiles_LastSelected; public string Profiles_LastSelected diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 8a289a632d..286a928728 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Linq; -using System.Security; using System.Text.Json; using System.Text.Json.Serialization; using System.Xml.Serialization; @@ -74,16 +73,21 @@ public static class SettingsManager /// /// Method to get the path of the settings folder. + /// Priority: + /// 1. Policy override (for IT administrators) + /// 2. Custom user-configured path (not available in portable mode) + /// 3. Portable (same directory as the application) or default location (Documents folder) /// /// Path to the settings folder. public static string GetSettingsFolderLocation() { // 1. Policy override takes precedence (for IT administrators) - if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation)) + if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.Settings_FolderLocation)) { - var validatedPath = ValidateSettingsFolderPath( - PolicyManager.Current.SettingsFolderLocation, + var validatedPath = DirectoryHelper.ValidateFolderPath( + PolicyManager.Current.Settings_FolderLocation, "Policy-provided", + nameof(PolicyInfo.Settings_FolderLocation), "next priority"); if (validatedPath != null) @@ -92,11 +96,12 @@ public static string GetSettingsFolderLocation() // 2. Custom user-configured path (not available in portable mode) if (!ConfigurationManager.Current.IsPortable && - !string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.SettingsFolderLocation)) + !string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.Settings_FolderLocation)) { - var validatedPath = ValidateSettingsFolderPath( - LocalSettingsManager.Current.SettingsFolderLocation, + var validatedPath = DirectoryHelper.ValidateFolderPath( + LocalSettingsManager.Current.Settings_FolderLocation, "Custom", + nameof(LocalSettingsInfo.Settings_FolderLocation), "default location"); if (validatedPath != null) @@ -129,67 +134,6 @@ public static string GetPortableSettingsFolderLocation() return Path.Combine(AssemblyManager.Current.Location, 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) - { - // Expand environment variables first (e.g. %userprofile%\settings -> C:\Users\...\settings) - path = Environment.ExpandEnvironmentVariables(path); - - // 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, SecurityException, PathTooLongException, or IOException if the path is invalid - var fullPath = Path.GetFullPath(path); - - // Check if the path is a directory (not a file) - if (File.Exists(fullPath)) - { - Log.Error($"{pathSource} SettingsFolderLocation is a file, not a directory: {path}. Falling back to {fallbackMessage}."); - return null; - } - - return Path.TrimEndingDirectorySeparator(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; - } - catch (PathTooLongException ex) - { - Log.Error($"{pathSource} SettingsFolderLocation path is too long: {path}. Falling back to {fallbackMessage}.", ex); - return null; - } - catch (IOException ex) - { - Log.Error($"{pathSource} SettingsFolderLocation caused an I/O error: {path}. Falling back to {fallbackMessage}.", ex); - return null; - } - } - /// /// Method to get the path of the settings backup folder. /// diff --git a/Source/NETworkManager.Settings/config.json.example b/Source/NETworkManager.Settings/config.json.example index 0a47ffa700..2bf8307cfb 100644 --- a/Source/NETworkManager.Settings/config.json.example +++ b/Source/NETworkManager.Settings/config.json.example @@ -1,4 +1,5 @@ { "Update_CheckForUpdatesAtStartup": false, - "SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings" + "Profiles_FolderLocation": "\\\\Server\\Shared\\NETworkManager\\Profiles", + "Settings_FolderLocation": "%UserProfile%\\NETworkManager\\Settings" } \ No newline at end of file diff --git a/Source/NETworkManager.Utilities/DirectoryHelper.cs b/Source/NETworkManager.Utilities/DirectoryHelper.cs index 25353b8cc1..c7aeef7efb 100644 --- a/Source/NETworkManager.Utilities/DirectoryHelper.cs +++ b/Source/NETworkManager.Utilities/DirectoryHelper.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Security; +using log4net; namespace NETworkManager.Utilities; @@ -8,6 +10,11 @@ namespace NETworkManager.Utilities; /// public static class DirectoryHelper { + /// + /// Logger for logging. + /// + private static readonly ILog Log = LogManager.GetLogger(typeof(DirectoryHelper)); + /// /// Create a directory with subdirectories and resolve environment variables. /// @@ -16,4 +23,67 @@ public static void CreateWithEnvironmentVariables(string path) { Directory.CreateDirectory(Environment.ExpandEnvironmentVariables(path)); } + + /// + /// Validates a folder path for correctness and accessibility. + /// Expands environment variables, checks that the path is absolute, + /// validates characters, and ensures it does not point to a file. + /// + /// The path to validate. + /// Description of the path source for logging (e.g., "Policy-provided", "Custom"). + /// Name of the property being validated for logging (e.g., "Settings_FolderLocation"). + /// Message describing what happens on validation failure (e.g., "next priority", "default location"). + /// The validated full path if valid; otherwise, null. + public static string ValidateFolderPath(string path, string pathSource, string propertyName, string fallbackMessage) + { + // Expand environment variables first (e.g. %userprofile%\settings -> C:\Users\...\settings) + path = Environment.ExpandEnvironmentVariables(path); + + // Validate that the path is rooted (absolute) + if (!Path.IsPathRooted(path)) + { + Log.Error($"{pathSource} {propertyName} is not an absolute path: {path}. Falling back to {fallbackMessage}."); + return null; + } + + // Validate that the path doesn't contain invalid characters + try + { + var fullPath = Path.GetFullPath(path); + + // Check if the path is a directory (not a file) + if (File.Exists(fullPath)) + { + Log.Error($"{pathSource} {propertyName} is a file, not a directory: {path}. Falling back to {fallbackMessage}."); + return null; + } + + return Path.TrimEndingDirectorySeparator(fullPath); + } + catch (ArgumentException ex) + { + Log.Error($"{pathSource} {propertyName} contains invalid characters: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } + catch (NotSupportedException ex) + { + Log.Error($"{pathSource} {propertyName} format is not supported: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } + catch (SecurityException ex) + { + Log.Error($"Insufficient permissions to access {pathSource} {propertyName}: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } + catch (PathTooLongException ex) + { + Log.Error($"{pathSource} {propertyName} path is too long: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } + catch (IOException ex) + { + Log.Error($"{pathSource} {propertyName} caused an I/O error: {path}. Falling back to {fallbackMessage}.", ex); + return null; + } + } } \ No newline at end of file diff --git a/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs b/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs index e2c58d980d..0173619963 100644 --- a/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsProfilesViewModel.cs @@ -7,6 +7,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; @@ -34,13 +35,69 @@ public string Location if (value == _location) return; + if (!_isLoading) + IsLocationChanged = !string.Equals(value, ProfileManager.GetProfilesFolderLocation(), StringComparison.OrdinalIgnoreCase); + _location = value; OnPropertyChanged(); } } + /// + /// Indicates whether the profiles location is managed by a system-wide policy. + /// + public bool IsLocationManagedByPolicy => !string.IsNullOrWhiteSpace(PolicyManager.Current?.Profiles_FolderLocation); + + /// + /// 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. + /// + private bool _isDefaultLocation; + + /// + /// Indicates whether the current location is the default profiles folder location. + /// + public bool IsDefaultLocation + { + get => _isDefaultLocation; + set + { + if (value == _isDefaultLocation) + return; + + _isDefaultLocation = value; + OnPropertyChanged(); + } + } + + /// + /// Private field of property. + /// private readonly ICollectionView _profileFiles; + /// + /// Gets the collection view of profile files. + /// public ICollectionView ProfileFiles { get => _profileFiles; @@ -54,8 +111,15 @@ private init } } + /// + /// Private field of property. + /// + private ProfileFileInfo _selectedProfileFile; + /// + /// Gets or sets the currently selected profile file information. + /// public ProfileFileInfo SelectedProfileFile { get => _selectedProfileFile; @@ -69,8 +133,15 @@ public ProfileFileInfo SelectedProfileFile } } + /// + /// Private field of property. + /// private bool _isDailyBackupEnabled; + + /// + /// Gets or sets a value indicating whether daily backups are enabled. + /// public bool IsDailyBackupEnabled { get => _isDailyBackupEnabled; @@ -87,8 +158,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; @@ -108,6 +185,9 @@ public int MaximumNumberOfBackups #region Constructor, LoadSettings + /// + /// Initializes a new instance of the class and loads the current profile files. + /// public SettingsProfilesViewModel() { _isLoading = true; @@ -123,9 +203,13 @@ public SettingsProfilesViewModel() _isLoading = false; } + /// + /// Load view specific settings. + /// private void LoadSettings() { Location = ProfileManager.GetProfilesFolderLocation(); + IsDefaultLocation = string.Equals(Location, ProfileManager.GetDefaultProfilesFolderLocation(), StringComparison.OrdinalIgnoreCase); IsDailyBackupEnabled = SettingsManager.Current.Profiles_IsDailyBackupEnabled; MaximumNumberOfBackups = SettingsManager.Current.Profiles_MaximumNumberOfBackups; } @@ -133,12 +217,99 @@ private void LoadSettings() #endregion #region ICommands & Actions + /// + /// 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. + /// + 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; + } + + + /// + /// Gets the command that initiates the action to change the location. + /// public ICommand OpenLocationCommand => new RelayCommand(_ => OpenLocationAction()); private static void OpenLocationAction() { Process.Start("explorer.exe", ProfileManager.GetProfilesFolderLocation()); + } + + /// + /// 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 profiles folder. + /// + /// A task that represents the asynchronous operation. + private async Task ChangeLocationAction() + { + var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, + Strings.ChangeLocationQuestion, + string.Format(Strings.ChangeProfilesLocationMessage, ProfileManager.GetProfilesFolderLocation(), Location), + ChildWindowIcon.Question, + Strings.Change); + + if (!result) + return; + + // Save profiles 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). + ProfileManager.Save(); + + // Set new location in SettingsInfo + SettingsManager.Current.Profiles_FolderLocation = Location; + + // Restart the application + (Application.Current.MainWindow as MainWindow)?.RestartApplication(); + } + + /// + /// Gets the command that restores the default location. + /// + public ICommand RestoreDefaultLocationCommand => new RelayCommand(_ => RestoreDefaultLocationActionAsync().ConfigureAwait(false)); + + /// + /// Restores the profiles folder location to the default path after obtaining user confirmation. + /// + /// A task that represents the asynchronous operation. + private async Task RestoreDefaultLocationActionAsync() + { + var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, + Strings.RestoreDefaultLocationQuestion, + string.Format(Strings.RestoreDefaultProfilesLocationMessage, ProfileManager.GetProfilesFolderLocation(), ProfileManager.GetDefaultProfilesFolderLocation()), + ChildWindowIcon.Question, + Strings.Restore); + + if (!result) + return; + + // Save profiles 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). + ProfileManager.Save(); + + // Clear custom location to revert to default + SettingsManager.Current.Profiles_FolderLocation = null; + + // Restart the application + (Application.Current.MainWindow as MainWindow)?.RestartApplication(); } public ICommand AddProfileFileCommand => new RelayCommand(async _ => await AddProfileFileAction().ConfigureAwait(false)); @@ -410,4 +581,15 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, } #endregion + + #region Methods + /// + /// Sets the location path based on the provided drag-and-drop input. + /// + /// The path to set as the location. + public void SetLocationPathFromDragDrop(string path) + { + Location = path; + } + #endregion } diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs index e14ee3639c..2a1b39439d 100644 --- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs @@ -50,7 +50,7 @@ public string Location /// /// Indicates whether the settings location is managed by a system-wide policy. /// - public bool IsLocationManagedByPolicy => !string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation); + public bool IsLocationManagedByPolicy => !string.IsNullOrWhiteSpace(PolicyManager.Current?.Settings_FolderLocation); /// /// Private field of property. @@ -159,8 +159,8 @@ public SettingsSettingsViewModel() } /// - /// Loads the application settings from the current settings folder location. - /// + /// Load view specific settings. + /// private void LoadSettings() { Location = SettingsManager.GetSettingsFolderLocation(); @@ -172,36 +172,6 @@ private void LoadSettings() #endregion #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); - } - - #endregion - - #region Methods /// /// Gets the command that opens the location folder selection dialog. /// @@ -210,10 +180,7 @@ private void ResetSettingsAction() /// /// 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(); @@ -228,12 +195,16 @@ private void BrowseLocationFolderAction() } /// - /// 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) + /// 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() { - Location = path; + Process.Start("explorer.exe", SettingsManager.GetSettingsFolderLocation()); } /// @@ -242,18 +213,14 @@ public void SetLocationPathFromDragDrop(string path) public ICommand ChangeLocationCommand => new RelayCommand(_ => ChangeLocationAction().ConfigureAwait(false)); /// - /// Prompts the user to confirm and then changes the location of the application's settings folder. + /// Prompts the user to confirm and then changes the location of the 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, Strings.ChangeLocationQuestion, - string.Format(Strings.ChangeLocationSettingsMessage, SettingsManager.GetSettingsFolderLocation(), Location), + string.Format(Strings.ChangeSettingsLocationMessage, SettingsManager.GetSettingsFolderLocation(), Location), ChildWindowIcon.Question, Strings.Change); @@ -265,7 +232,7 @@ private async Task ChangeLocationAction() SettingsManager.Save(); // Set new location - LocalSettingsManager.Current.SettingsFolderLocation = Location; + LocalSettingsManager.Current.Settings_FolderLocation = Location; LocalSettingsManager.Save(); // Restart the application @@ -289,7 +256,7 @@ 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.RestoreDefaultSettingsLocationMessage, SettingsManager.GetSettingsFolderLocation(), SettingsManager.GetDefaultSettingsFolderLocation()), ChildWindowIcon.Question, Strings.Restore); @@ -301,7 +268,7 @@ private async Task RestoreDefaultLocationActionAsync() SettingsManager.Save(); // Clear custom location to revert to default - LocalSettingsManager.Current.SettingsFolderLocation = null; + LocalSettingsManager.Current.Settings_FolderLocation = null; LocalSettingsManager.Save(); // Restart the application @@ -309,19 +276,21 @@ private async Task RestoreDefaultLocationActionAsync() } /// - /// Resets the application settings to their default values and restarts the application after user confirmation. + /// Gets the command that resets the application settings to their default values. + /// + public ICommand ResetSettingsCommand => new RelayCommand(_ => ResetSettingsAction().ConfigureAwait(false)); + + /// + /// Resets the application settings to their default values. /// - /// 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() + private async Task ResetSettingsAction() { var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow, - Strings.ResetSettingsQuestion, - Strings.SettingsAreResetAndApplicationWillBeRestartedMessage, - ChildWindowIcon.Question, - Strings.Reset); + Strings.ResetSettingsQuestion, + Strings.SettingsAreResetAndApplicationWillBeRestartedMessage, + ChildWindowIcon.Question, + Strings.Reset); if (!result) return; @@ -332,5 +301,17 @@ private async Task ResetSettings() // Restart the application (Application.Current.MainWindow as MainWindow)?.RestartApplication(); } + + #endregion + + #region Methods + /// + /// Sets the location path based on the provided drag-and-drop input. + /// + /// The path to set as the location. + public void SetLocationPathFromDragDrop(string path) + { + Location = path; + } #endregion } diff --git a/Source/NETworkManager/Views/SettingsProfilesView.xaml b/Source/NETworkManager/Views/SettingsProfilesView.xaml index 0989c4a24d..efa4d9c5d4 100644 --- a/Source/NETworkManager/Views/SettingsProfilesView.xaml +++ b/Source/NETworkManager/Views/SettingsProfilesView.xaml @@ -10,36 +10,182 @@ xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:profiles="clr-namespace:NETworkManager.Profiles;assembly=NETworkManager.Profiles" + xmlns:validators="clr-namespace:NETworkManager.Validators;assembly=NETworkManager.Validators" + xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings" mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:SettingsProfilesViewModel}" Loaded="UserControl_Loaded"> + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 }) + _viewModel.SetLocationPathFromDragDrop(files[0]); + } + + private void TextBoxLocation_PreviewDragOver(object sender, DragEventArgs e) + { + e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) + ? DragDropEffects.Copy + : DragDropEffects.None; + e.Handled = true; + } } \ No newline at end of file diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml index d1db5b42be..44246e210f 100644 --- a/Source/NETworkManager/Views/SettingsSettingsView.xaml +++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml @@ -18,7 +18,8 @@ - + - - + - - + +