/*
* 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 Microsoft.UI.Xaml.Input;
using ProtonVPN.Client.Common.Collections;
using ProtonVPN.Client.Core.Bases;
using ProtonVPN.Client.Core.Bases.ViewModels;
using ProtonVPN.Client.Core.Models;
using ProtonVPN.Client.Core.Services.Activation;
using ProtonVPN.Client.Services.Bootstrapping.Helpers;
using ProtonVPN.Client.UI.Overlays.Selection.Contracts;
using ProtonVPN.Common.Core.Networking;
using Windows.System;
namespace ProtonVPN.Client.UI.Overlays.Selection;
public partial class IpSelectorOverlayViewModel : OverlayViewModelBase, IIpSelector
{
private bool _isRunningAsAdmin;
private List _originalAddresses = [];
[ObservableProperty]
private string _title = string.Empty;
[ObservableProperty]
private string _description = string.Empty;
[ObservableProperty]
private string _caption = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDragDropEnabled))]
private bool _canReorder;
[ObservableProperty]
private bool _isAddressRangeAuthorized;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(AddAddressCommand))]
private string _currentAddress = string.Empty;
[ObservableProperty]
private string? _currentAddressError;
public SmartNotifyObservableCollection Addresses { get; } = [];
public bool HasAddresses => Addresses.Count > 0;
public string MoveUpTooltip => Localizer.Get("Common_Actions_MoveUp");
public string MoveDownTooltip => Localizer.Get("Common_Actions_MoveDown");
public string RemoveTooltip => Localizer.Get("Common_Actions_Remove");
public bool IsDragDropEnabled => CanReorder && !_isRunningAsAdmin;
public bool HasChanges => !AreAddressesEqual(_originalAddresses, Addresses);
public IpSelectorOverlayViewModel(
IMainWindowOverlayActivator overlayActivator,
IViewModelHelper viewModelHelper)
: base(overlayActivator, viewModelHelper)
{
_isRunningAsAdmin = AppInstanceHelper.IsRunningAsAdmin();
Addresses.CollectionChanged += OnAddressesCollectionChanged;
Addresses.ItemPropertyChanged += OnAddressesItemPropertyChanged;
}
public async Task?> SelectAsync(List addresses)
{
ResetCurrentAddress();
ResetCurrentAddressError();
_originalAddresses = addresses.Select(a => a.Clone()).ToList();
Addresses.Reset(addresses);
ContentDialogResult result = await InvokeAsync();
return result switch
{
ContentDialogResult.Primary => Addresses.ToList(),
_ => null
};
}
public void OnCurrentAddressKeyDownHandler(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
AddAddress();
}
}
protected override void OnLanguageChanged()
{
base.OnLanguageChanged();
OnPropertyChanged(nameof(MoveUpTooltip));
OnPropertyChanged(nameof(MoveDownTooltip));
OnPropertyChanged(nameof(RemoveTooltip));
}
private void OnAddressesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasAddresses));
OnPropertyChanged(nameof(HasChanges));
MoveAddressUpCommand.NotifyCanExecuteChanged();
MoveAddressDownCommand.NotifyCanExecuteChanged();
}
private void OnAddressesItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(nameof(HasChanges));
}
[RelayCommand(CanExecute = nameof(CanAddAddress))]
private void AddAddress()
{
NetworkAddress? address = GetValidatedCurrentIpAddress();
string? error = GetIpAddressError(address);
if (error != null || address == null)
{
CurrentAddressError = error ?? Localizer.Get("Settings_Common_IpAddresses_Invalid");
return;
}
Addresses.Add(new SelectableNetworkAddress(address.Value));
ResetCurrentAddress();
}
private bool CanAddAddress()
{
return !string.IsNullOrWhiteSpace(CurrentAddress);
}
[RelayCommand]
private void RemoveAddress(SelectableNetworkAddress address)
{
Addresses.Remove(address);
}
[RelayCommand(CanExecute = nameof(CanMoveAddressUp))]
private void MoveAddressUp(SelectableNetworkAddress address)
{
int currentIndex = Addresses.IndexOf(address);
if (currentIndex > 0)
{
Addresses.Move(currentIndex, currentIndex - 1);
}
}
private bool CanMoveAddressUp(SelectableNetworkAddress address)
{
int currentIndex = Addresses.IndexOf(address);
return CanReorder
&& currentIndex > 0;
}
[RelayCommand(CanExecute = nameof(CanMoveAddressDown))]
private void MoveAddressDown(SelectableNetworkAddress address)
{
int currentIndex = Addresses.IndexOf(address);
if (currentIndex >= 0 && currentIndex < Addresses.Count - 1)
{
Addresses.Move(currentIndex, currentIndex + 1);
}
}
private bool CanMoveAddressDown(SelectableNetworkAddress address)
{
int currentIndex = Addresses.IndexOf(address);
return CanReorder
&& currentIndex >= 0
&& currentIndex < Addresses.Count - 1;
}
private NetworkAddress? GetValidatedCurrentIpAddress()
{
return NetworkAddress.TryParse(CurrentAddress, out NetworkAddress address) ? address : null;
}
private string? GetIpAddressError(NetworkAddress? address)
{
if (address == null)
{
return Localizer.Get("Settings_Common_IpAddresses_Invalid");
}
if (!IsAddressRangeAuthorized && !address.Value.IsSingleIp)
{
return Localizer.Get("Settings_Common_IpAddresses_Invalid");
}
if (Addresses.FirstOrDefault(ip => ip.Value.FormattedAddress == address.Value.FormattedAddress) != null)
{
return Localizer.Get("Settings_Common_IpAddresses_AlreadyExists");
}
return null;
}
private void ResetCurrentAddress()
{
CurrentAddress = string.Empty;
}
private void ResetCurrentAddressError()
{
CurrentAddressError = null;
}
partial void OnCurrentAddressChanged(string value)
{
ResetCurrentAddressError();
}
private bool AreAddressesEqual(List original, IList current)
{
if (original.Count != current.Count)
{
return false;
}
for (int i = 0; i < original.Count; i++)
{
if (original[i].Value.FormattedAddress != current[i].Value.FormattedAddress ||
original[i].IsSelected != current[i].IsSelected)
{
return false;
}
}
return true;
}
}