/*
* 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.Collections.Specialized;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml.Controls;
using ProtonVPN.Client.Common.Collections;
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.UI.Overlays.Selection.Contracts;
namespace ProtonVPN.Client.UI.Overlays.Selection;
public partial class AppSelectorOverlayViewModel : OverlayViewModelBase, IAppSelector
{
private readonly IMainWindowActivator _mainWindowActivator;
private List _originalApps = [];
[ObservableProperty]
private string _title = string.Empty;
[ObservableProperty]
private string _description = string.Empty;
public string RemoveTooltip => Localizer.Get("Common_Actions_Remove");
public SmartNotifyObservableCollection Apps { get; } = [];
public bool HasApps => Apps.Count > 0;
public bool HasChanges => !AreAppsEqual(_originalApps, Apps);
public AppSelectorOverlayViewModel(
IMainWindowActivator mainWindowActivator,
IMainWindowOverlayActivator overlayActivator,
IViewModelHelper viewModelHelper)
: base(overlayActivator, viewModelHelper)
{
_mainWindowActivator = mainWindowActivator;
Apps.CollectionChanged += OnAppsCollectionChanged;
Apps.ItemPropertyChanged += OnAppsItemPropertyChanged;
}
public async Task?> SelectAsync(List apps)
{
_originalApps = apps.Select(a => a.Clone()).ToList();
Apps.Reset(apps);
ContentDialogResult result = await InvokeAsync();
return result switch
{
ContentDialogResult.Primary => Apps.ToList(),
_ => null
};
}
protected override void OnLanguageChanged()
{
base.OnLanguageChanged();
OnPropertyChanged(nameof(RemoveTooltip));
}
private static bool IsSameAppPath(string filePathA, string filePathB)
{
return string.Equals(filePathA, filePathB, StringComparison.OrdinalIgnoreCase);
}
private void OnAppsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasApps));
OnPropertyChanged(nameof(HasChanges));
}
private void OnAppsItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(nameof(HasChanges));
}
[RelayCommand]
private async Task AddAppAsync()
{
if (_mainWindowActivator.Window == null)
{
return;
}
string filePath = await _mainWindowActivator.Window.PickSingleFileAsync(Localizer.Get("Settings_Connection_SplitTunneling_Apps_FilesFilterName"), [ExternalApp.EXE_FILE_EXTENSION]);
TunnelingApp? app = await TunnelingApp.TryCreateAsync(filePath);
if (app == null)
{
return;
}
SelectableTunnelingApp? existingApp = Apps.FirstOrDefault(a => IsSameAppPath(a.Value.AppPath, app.AppPath) || a.Value.AlternateAppPaths.Any(alt => IsSameAppPath(alt, app.AppPath)));
if (existingApp != null)
{
existingApp.IsSelected = true;
}
else
{
Apps.Add(new SelectableTunnelingApp(app));
}
}
[RelayCommand]
private void RemoveApp(SelectableTunnelingApp app)
{
Apps.Remove(app);
}
private bool AreAppsEqual(List original, IList current)
{
if (original.Count != current.Count)
{
return false;
}
for (int i = 0; i < original.Count; i++)
{
if (original[i].Value.AppPath != current[i].Value.AppPath ||
!original[i].Value.AlternateAppPaths.SequenceEqual(current[i].Value.AlternateAppPaths) ||
original[i].IsSelected != current[i].IsSelected)
{
return false;
}
}
return true;
}
}