/*
* Copyright (c) 2025 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.Security;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ProtonVPN.Client.Common.Collections;
using ProtonVPN.Client.Common.Enums;
using ProtonVPN.Client.Core.Bases;
using ProtonVPN.Client.Core.Bases.ViewModels;
using ProtonVPN.Client.Core.Extensions;
using ProtonVPN.Client.Core.Models;
using ProtonVPN.Client.Core.Services.Activation;
using ProtonVPN.Client.Factories;
using ProtonVPN.Client.Logic.Profiles.Contracts;
using ProtonVPN.Client.Logic.Profiles.Contracts.Models;
using ProtonVPN.Client.Models.Profiles;
using ProtonVPN.Client.UI.Main.Profiles.Contracts;
using ProtonVPN.Common.Core.Extensions;
namespace ProtonVPN.Client.UI.Main.Profiles.Components;
public partial class ProfileOptionsSelectorViewModel : ViewModelBase, IProfileOptionsSelector
{
private readonly IMainWindowActivator _mainWindowActivator;
private readonly ICommonItemFactory _commonItemFactory;
private static readonly ConnectAndGoMode?[] _connectAndGoModes =
{
null,
ConnectAndGoMode.Website,
ConnectAndGoMode.Application
};
private IProfileOptions _originalProfileOptions = ProfileOptions.Default;
[ObservableProperty]
private bool _isEnabled;
[ObservableProperty]
private ConnectAndGoModeItem? _selectedConnectAndGoMode;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsConnectAndGoUrlValid))]
[NotifyPropertyChangedFor(nameof(ConnectAndGoUrlErrorMessage))]
private string _connectAndGoUrl = string.Empty;
[ObservableProperty]
private bool _usePrivateBrowsingMode;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ConnectAndGoAppName))]
private ExternalApp? _connectAndGoApp;
public bool IsConnectAndGoUrlValid => string.IsNullOrEmpty(ConnectAndGoUrl) || ConnectAndGoUrl.IsValidUrl();
public string ConnectAndGoUrlErrorMessage => !IsConnectAndGoUrlValid
? Localizer.Get("Profile_Options_Url_Error")
: string.Empty;
public string ConnectAndGoAppName => ConnectAndGoApp?.AppName ?? Localizer.Get("Profile_Options_Application_Select");
public SmartObservableCollection ConnectAndGoModes { get; } = [];
private bool ConnectAndGoStateHasChanged => _originalProfileOptions.ConnectAndGo.IsEnabled != SelectedConnectAndGoMode?.IsEnabled;
private bool ConnectAndGoModeHasChanged => _originalProfileOptions.ConnectAndGo.Mode != SelectedConnectAndGoMode?.Mode;
public ProfileOptionsSelectorViewModel(
IMainWindowActivator mainWindowActivator,
ICommonItemFactory commonItemFactory,
IViewModelHelper viewModelHelper)
: base(viewModelHelper)
{
_mainWindowActivator = mainWindowActivator;
_commonItemFactory = commonItemFactory;
}
public IProfileOptions GetProfileOptions()
{
// Turn off Connect and go on save if the URL or application is not valid
bool isConnectAndGoEnabled =
SelectedConnectAndGoMode != null &&
SelectedConnectAndGoMode.IsEnabled &&
SelectedConnectAndGoMode.Mode switch
{
ConnectAndGoMode.Website => ConnectAndGoUrl.IsValidUrl(),
ConnectAndGoMode.Application => ConnectAndGoApp?.IsValid ?? false,
_ => false
};
return new ProfileOptions()
{
ConnectAndGo = new ConnectAndGoOption()
{
IsEnabled = isConnectAndGoEnabled,
Mode = isConnectAndGoEnabled && SelectedConnectAndGoMode != null
? SelectedConnectAndGoMode.Mode
: DefaultProfileSettings.ConnectAndGoMode,
UsePrivateBrowsingMode = UsePrivateBrowsingMode,
Url = ConnectAndGoUrl.Trim(),
AppPath = ConnectAndGoApp?.AppPath
}
};
}
public async Task SetProfileOptionsAsync(IProfileOptions options)
{
_originalProfileOptions = options ?? ProfileOptions.Default;
InvalidateCollections();
SelectedConnectAndGoMode = _originalProfileOptions.ConnectAndGo.IsEnabled
? ConnectAndGoModes.FirstOrDefault(m => m.IsEnabled && _originalProfileOptions.ConnectAndGo.Mode == m.Mode)
: ConnectAndGoModes.FirstOrDefault(m => !m.IsEnabled);
UsePrivateBrowsingMode = _originalProfileOptions.ConnectAndGo.UsePrivateBrowsingMode;
ConnectAndGoUrl = _originalProfileOptions.ConnectAndGo.Url;
ConnectAndGoApp = await GetExternalAppAsync(_originalProfileOptions.ConnectAndGo.AppPath);
}
public bool HasChanged()
{
return ConnectAndGoStateHasChanged
|| ConnectAndGoModeHasChanged
|| SelectedConnectAndGoMode?.Mode switch
{
ConnectAndGoMode.Website =>
_originalProfileOptions.ConnectAndGo.Url != ConnectAndGoUrl.Trim() ||
_originalProfileOptions.ConnectAndGo.UsePrivateBrowsingMode != UsePrivateBrowsingMode,
ConnectAndGoMode.Application =>
_originalProfileOptions.ConnectAndGo.AppPath != ConnectAndGoApp?.AppPath,
_ => false
};
}
public bool IsReconnectionRequired()
{
return false;
}
[RelayCommand]
public async Task SelectAppAsync()
{
if (_mainWindowActivator.Window == null)
{
return;
}
string appFilePath = await _mainWindowActivator.Window.PickSingleFileAsync(Localizer.Get("Settings_Connection_SplitTunneling_Apps_FilesFilterName"), [ExternalApp.EXE_FILE_EXTENSION]);
ConnectAndGoApp = await GetExternalAppAsync(appFilePath);
}
private void InvalidateCollections()
{
ConnectAndGoModes.Reset(_connectAndGoModes.Select(_commonItemFactory.GetConnectAndGoMode));
}
[RelayCommand]
private void DisableConnectAndGo()
{
SelectConnectAndGoMode(false);
}
[RelayCommand]
private void EnableConnectAndGoToWebsite()
{
SelectConnectAndGoMode(true, ConnectAndGoMode.Website);
}
[RelayCommand]
private void EnableConnectAndGoToApplication()
{
SelectConnectAndGoMode(true, ConnectAndGoMode.Application);
}
private void SelectConnectAndGoMode(bool isEnabled, ConnectAndGoMode? mode = null)
{
SelectedConnectAndGoMode = isEnabled
? ConnectAndGoModes.FirstOrDefault(nsm => nsm.IsEnabled && nsm.Mode == mode)
: ConnectAndGoModes.FirstOrDefault(nsm => !nsm.IsEnabled);
}
protected override void OnLanguageChanged()
{
base.OnLanguageChanged();
OnPropertyChanged(nameof(ConnectAndGoUrlErrorMessage));
OnPropertyChanged(nameof(ConnectAndGoAppName));
}
private async Task GetExternalAppAsync(string? appFilePath)
{
if (string.IsNullOrEmpty(appFilePath))
{
return null;
}
return await ExternalApp.TryCreateAsync(appFilePath)
?? ExternalApp.NotFound(appFilePath, Localizer.Get("Common_Message_AppNotFound"));
}
}