/* * 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 CommunityToolkit.Mvvm.ComponentModel; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Enums; using ProtonVPN.Client.Core.Services.Navigation; using ProtonVPN.Client.EventMessaging.Contracts; using ProtonVPN.Client.Factories; using ProtonVPN.Client.Localization.Extensions; using ProtonVPN.Client.Logic.Connection.Contracts; using ProtonVPN.Client.Logic.Connection.Contracts.Messages; using ProtonVPN.Client.Logic.Searches.Contracts; using ProtonVPN.Client.Logic.Servers.Contracts; using ProtonVPN.Client.Logic.Servers.Contracts.Enums; using ProtonVPN.Client.Logic.Servers.Contracts.Extensions; using ProtonVPN.Client.Logic.Servers.Contracts.Messages; using ProtonVPN.Client.Logic.Servers.Contracts.Models; using ProtonVPN.Client.Logic.Servers.Contracts.Searches; using ProtonVPN.Client.Models.Connections; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Client.UI.Main.Sidebar.Bases; using ProtonVPN.Client.UI.Main.Sidebar.Connections.Bases.Contracts; using ProtonVPN.Client.UI.Main.Sidebar.Search.Contracts; namespace ProtonVPN.Client.UI.Main.Sidebar.Search; public partial class SearchResultsPageViewModel : ConnectionListViewModelBase, ISearchInputReceiver, IEventMessageReceiver, IEventMessageReceiver, IEventMessageReceiver, IEventMessageReceiver { private readonly IGlobalSearch _globalSearch; private readonly ILocationItemFactory _locationItemFactory; private readonly IServerFinder _serverFinder; private string _input = string.Empty; [ObservableProperty] private bool _hasSearchInput; [ObservableProperty] private ICountriesComponent _selectedCountriesComponent; public List CountriesComponents { get; } public string ExampleCountries => $"{Localizer.GetCountryName("JP")}, {Localizer.GetCountryName("US")}"; public string ExampleCities => $"{Localizer.GetCityName("Tokyo", "JP")}, {Localizer.GetCityName("Los Angeles", "US")}"; public string ExampleServers => "JP#75, US-NY#166"; public SearchResultsPageViewModel( ISettings settings, IConnectionManager connectionManager, IServersLoader serversLoader, ISidebarViewNavigator parentViewNavigator, IGlobalSearch globalSearch, ILocationItemFactory locationItemFactory, IConnectionGroupFactory connectionGroupFactory, IEnumerable countriesComponents, IViewModelHelper viewModelHelper, IServerFinder serverFinder) : base(parentViewNavigator, settings, serversLoader, connectionManager, connectionGroupFactory, viewModelHelper) { _globalSearch = globalSearch; _locationItemFactory = locationItemFactory; _serverFinder = serverFinder; CountriesComponents = new(countriesComponents.OrderBy(p => p.SortIndex)); _selectedCountriesComponent = CountriesComponents.First(); } protected override void OnLanguageChanged() { base.OnLanguageChanged(); OnPropertyChanged(nameof(ExampleCountries)); SearchAsync().Wait(); } partial void OnSelectedCountriesComponentChanged(ICountriesComponent value) { SearchAsync().Wait(); } public async Task SearchAsync(string input) { _input = input; await SearchAsync(); } private async Task SearchAsync() { string input = _input; if (string.IsNullOrWhiteSpace(input)) { HasSearchInput = false; SetSearchResult([]); _serverFinder.Cancel(); } else { HasSearchInput = true; IEnumerable result = await SetSearchResultsAsync(input); TriggerServerSearchTimerIfNecessary(input, result); } } private async Task> SetSearchResultsAsync(string input) { IEnumerable result = (await _globalSearch.SearchAsync(input, GetServerFeatures())) .Select(GetConnectionItemCreationFunction()) .Where(ci => ci is not null) .Cast(); SetSearchResult(result); return result; } private void TriggerServerSearchTimerIfNecessary(string input, IEnumerable result) { if (!result.Where(r => r is ServerLocationItemBase slib && DoesInputMatchServerName(input, slib.Server.Name)).Any()) { _serverFinder.Search(input); } else { _serverFinder.Cancel(); } } private bool DoesInputMatchServerName(string input, string serverName) { return string.Equals(TrimServerName(input), TrimServerName(serverName), StringComparison.InvariantCultureIgnoreCase); } private string TrimServerName(string input) { return input.Replace("#", "").Replace("-", "").Replace(" ", ""); } private ServerFeatures? GetServerFeatures() { return SelectedCountriesComponent.ConnectionType switch { CountriesConnectionType.SecureCore => ServerFeatures.SecureCore, CountriesConnectionType.P2P => ServerFeatures.P2P, CountriesConnectionType.Tor => ServerFeatures.Tor, _ => null, }; } private void SetSearchResult(IEnumerable result) { ResetItems(result); ResetGroups(); InvalidateActiveConnection(); InvalidateMaintenanceStates(); InvalidateRestrictions(); OnPropertyChanged(nameof(HasItems)); } private Func GetConnectionItemCreationFunction() { return SelectedCountriesComponent.ConnectionType switch { CountriesConnectionType.SecureCore => CreateSecureCoreConnectionItem, CountriesConnectionType.P2P => CreateP2PConnectionItem, CountriesConnectionType.Tor => CreateTorConnectionItem, _ => CreateStandardConnectionItem, }; } private ConnectionItemBase? CreateSecureCoreConnectionItem(ILocation location) { if (location is Server server) { return _locationItemFactory.GetServer(server, isSearchItem: true); } else if (location is Country country) { return _locationItemFactory.GetSecureCoreCountry(country, isSearchItem: true); } return null; } private ConnectionItemBase? CreateP2PConnectionItem(ILocation location) { if (location is Server server) { return _locationItemFactory.GetP2PServer(server, isSearchItem: true); } else if (location is City city) { return _locationItemFactory.GetP2PCity(city, isSearchItem: true); } else if (location is State state) { return _locationItemFactory.GetP2PState(state, isSearchItem: true); } else if (location is Country country) { return _locationItemFactory.GetP2PCountry(country, isSearchItem: true); } return null; } private ConnectionItemBase? CreateTorConnectionItem(ILocation location) { if (location is Server server) { return _locationItemFactory.GetTorServer(server, isSearchItem: true); } else if (location is Country country) { return _locationItemFactory.GetTorCountry(country, isSearchItem: true); } return null; } private ConnectionItemBase? CreateStandardConnectionItem(ILocation location) { if (location is Server server) { if (server.Features.IsB2B()) { return _locationItemFactory.GetGatewayServer(server); } else { return _locationItemFactory.GetServer(server, isSearchItem: true); } } else if (location is City city) { return _locationItemFactory.GetCity(city, isSearchItem: true); } else if (location is State state) { return _locationItemFactory.GetState(state, isSearchItem: true); } else if (location is Country country) { return _locationItemFactory.GetCountry(country, isSearchItem: true); } return null; } public void Receive(ConnectionStatusChangedMessage message) { ExecuteOnUIThread(InvalidateActiveConnection); } public void Receive(ServerListChangedMessage message) { ExecuteOnUIThread(() => { InvalidateActiveConnection(); InvalidateMaintenanceStates(); InvalidateRestrictions(); }); } public void Receive(NewServerFoundMessage message) { string input = _input; if (string.IsNullOrWhiteSpace(input)) { return; } ExecuteOnUIThread(async () => { await SetSearchResultsAsync(input); }); } public void Receive(LocationNamesChangedMessage message) { ExecuteOnUIThread(() => SearchAsync().Wait()); } }