/* * Copyright (c) 2024 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.UI.Xaml.Controls; using ProtonVPN.Client.Contracts.Profiles; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Bases.ViewModels; using ProtonVPN.Client.Core.Services.Activation; using ProtonVPN.Client.Core.Services.Navigation; using ProtonVPN.Client.Logic.Connection.Contracts; using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents; using ProtonVPN.Client.Logic.Profiles.Contracts; using ProtonVPN.Client.Logic.Profiles.Contracts.Models; using ProtonVPN.Client.UI.Main.Profiles.Contracts; using ProtonVPN.Configurations.Contracts; using ProtonVPN.StatisticalEvents.Contracts.Dimensions; namespace ProtonVPN.Client.UI.Main.Profiles; public partial class ProfilePageViewModel : PageViewModelBase, IProfileEditor, IProfilePropertiesSelector { private readonly IMainWindowOverlayActivator _mainWindowOverlayActivator; private readonly IConnectionManager _connectionManager; private readonly IProfilesManager _profilesManager; private readonly IProfileIconSelector _profileIconSelector; private readonly IProfileSettingsSelector _profileSettingsSelector; private readonly IProfileOptionsSelector _profileOptionsSelector; private readonly IConnectionIntentSelector _connectionIntentSelector; private readonly IConfiguration _configuration; private readonly IVpnServiceSettingsUpdater _vpnServiceSettingsUpdater; private IConnectionProfile _profile; private bool _isNewProfile; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsProfileNameValid))] [NotifyCanExecuteChangedFor(nameof(SaveProfileCommand))] private string _profileName = string.Empty; public bool IsProfileNameValid => !string.IsNullOrWhiteSpace(ProfileName); public int MaximumProfileNameLength => _configuration.MaximumProfileNameLength; public string ApplyCommandText => Localizer.Get(IsReconnectionRequired() ? "Common_Actions_Reconnect" : "Common_Actions_Save"); public ProfilePageViewModel( IMainViewNavigator parentViewNavigator, IViewModelHelper viewModelHelper, IMainWindowOverlayActivator mainWindowOverlayActivator, IConnectionManager connectionManager, IProfilesManager profilesManager, IProfileIconSelector profileIconSelector, IProfileSettingsSelector profileSettingsSelector, IProfileOptionsSelector profileOptionsSelector, IConnectionIntentSelector connectionIntentSelector, IConfiguration configuration, IVpnServiceSettingsUpdater vpnServiceSettingsUpdater) : base(parentViewNavigator, viewModelHelper) { _mainWindowOverlayActivator = mainWindowOverlayActivator; _connectionManager = connectionManager; _profilesManager = profilesManager; _profileIconSelector = profileIconSelector; _profileSettingsSelector = profileSettingsSelector; _profileOptionsSelector = profileOptionsSelector; _connectionIntentSelector = connectionIntentSelector; _configuration = configuration; _vpnServiceSettingsUpdater = vpnServiceSettingsUpdater; _profile = ConnectionProfile.Default; _profileIconSelector.PropertyChanged += OnSelectorPropertyChanged; _connectionIntentSelector.PropertyChanged += OnSelectorPropertyChanged; _profileSettingsSelector.PropertyChanged += OnSelectorPropertyChanged; _profileOptionsSelector.PropertyChanged += OnSelectorPropertyChanged; } public async Task CreateProfileAsync() { await EditProfileAsync(ConnectionProfile.Default); _isNewProfile = true; SaveProfileCommand.NotifyCanExecuteChanged(); } public async Task EditProfileAsync(IConnectionProfile profile) { _profile = profile; _isNewProfile = false; ProfileName = _profile.Name; _profileIconSelector.SetProfileIcon(_profile.Icon); _profileSettingsSelector.SetProfileSettings(_profile.Settings); await _profileOptionsSelector.SetProfileOptionsAsync(_profile.Options); _connectionIntentSelector.SetConnectionIntent(_profile); await InvokeAsync(); } public async Task DuplicateProfileAsync(IConnectionProfile profile) { string profileName = Localizer.GetFormat("Connections_Profiles_Name_Duplicate", profile.Name); await EditProfileAsync(profile.Duplicate(profileName)); _isNewProfile = true; SaveProfileCommand.NotifyCanExecuteChanged(); } public async Task TryRedirectToProfileAsync(string overriddenSettingsName, IConnectionProfile profile) { ContentDialogResult result = await _mainWindowOverlayActivator.ShowSettingsOverriddenByProfileOverlayAsync(overriddenSettingsName); if (result == ContentDialogResult.Primary) { await EditProfileAsync(profile); return true; } return false; } [RelayCommand] public async Task CloseAsync() { bool navigationCompleted = await ParentViewNavigator.NavigateToHomeViewAsync(); if (navigationCompleted) { RequestResetContentScroll(); } return navigationCompleted; } public override async Task CanNavigateFromAsync() { if (!CanSaveProfile()) // No changes made, simply leave page { return true; } ContentDialogResult result = await _mainWindowOverlayActivator.ShowDiscardConfirmationOverlayAsync(); return result switch { // Do nothing, user decided to discard profile changes. ContentDialogResult.Primary => true, // Save profile and trigger reconnection if needed ContentDialogResult.Secondary => await SaveAsync(), // Cancel navigation, stays on current page without deleting changes user have made _ => false, }; } public bool HasChanged() { return _isNewProfile || _profile.Name != ProfileName || _profileIconSelector.HasChanged() || _profileSettingsSelector.HasChanged() || _profileOptionsSelector.HasChanged() || _connectionIntentSelector.HasChanged(); } public bool IsReconnectionRequired() { if (!IsConnectedToProfile()) { return false; } return _profileIconSelector.IsReconnectionRequired() || _profileSettingsSelector.IsReconnectionRequired() || _profileOptionsSelector.IsReconnectionRequired() || _connectionIntentSelector.IsReconnectionRequired(); } protected override void OnPropertyChanged(PropertyChangedEventArgs e) { base.OnPropertyChanged(e); if (e.PropertyName != nameof(ApplyCommandText)) { OnProfilePropertyChanged(); } } private bool IsConnectedToProfile() { return !_connectionManager.IsDisconnected && _connectionManager.CurrentConnectionIntent?.IsSameAs(_profile) == true; } [RelayCommand(CanExecute = nameof(CanSaveProfile))] private async Task SaveProfileAsync() { return await SaveAsync() && await CloseAsync(); } private bool CanSaveProfile() { return HasChanged(); } private async Task SaveAsync() { bool isReconnectionRequired = IsReconnectionRequired(); bool haveSettingsChanged = _profileSettingsSelector.HasChanged(); _profile.Name = IsProfileNameValid ? ProfileName.Trim() : Localizer.Get("Connections_Profiles_Name_Default"); _profile.Icon = _profileIconSelector.GetProfileIcon(); _profile.Settings = _profileSettingsSelector.GetProfileSettings(); _profile.Options = _profileOptionsSelector.GetProfileOptions(); IConnectionIntent connectionIntent = _connectionIntentSelector.GetConnectionIntent(); _profile.UpdateIntent(connectionIntent.Location, connectionIntent.Feature); _profilesManager.AddOrEditProfile(_profile); await EditProfileAsync(_profile); if (isReconnectionRequired) { return await _connectionManager.ReconnectAsync(VpnTriggerDimension.NewConnection); } else if (IsConnectedToProfile() && haveSettingsChanged) { await _vpnServiceSettingsUpdater.SendAsync(); } return true; } private void OnSelectorPropertyChanged(object? sender, PropertyChangedEventArgs e) { OnProfilePropertyChanged(); } private void OnProfilePropertyChanged() { OnPropertyChanged(nameof(ApplyCommandText)); SaveProfileCommand.NotifyCanExecuteChanged(); } }