2022-04-12 13:09:14 +00:00
|
|
|
//
|
|
|
|
// ProviderLocationView.swift
|
|
|
|
// Passepartout
|
|
|
|
//
|
|
|
|
// Created by Davide De Rosa on 2/19/22.
|
2023-03-17 15:56:19 +00:00
|
|
|
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
|
2022-04-12 13:09:14 +00:00
|
|
|
//
|
|
|
|
// https://github.com/passepartoutvpn
|
|
|
|
//
|
|
|
|
// This file is part of Passepartout.
|
|
|
|
//
|
|
|
|
// Passepartout 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.
|
|
|
|
//
|
|
|
|
// Passepartout 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 Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//
|
|
|
|
|
2022-06-23 21:31:01 +00:00
|
|
|
import PassepartoutLibrary
|
2023-04-04 07:50:45 +00:00
|
|
|
import SwiftUI
|
2022-04-12 13:09:14 +00:00
|
|
|
|
2022-04-19 06:56:10 +00:00
|
|
|
struct ProviderLocationView: View, ProviderProfileAvailability {
|
2022-08-28 07:19:15 +00:00
|
|
|
@ObservedObject var providerManager: ProviderManager
|
2022-04-19 06:56:10 +00:00
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
@ObservedObject private var currentProfile: ObservableProfile
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2022-04-19 06:56:10 +00:00
|
|
|
private let isEditable: Bool
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
@Binding private var selectedServer: ProviderServer?
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
@Binding private var favoriteLocationIds: Set<String>?
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2022-06-15 18:53:37 +00:00
|
|
|
@AppStorage(AppPreference.isShowingFavorites.key) private var isShowingFavorites = false
|
2022-04-12 13:09:14 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
var profile: Profile {
|
|
|
|
currentProfile.value
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding<Bool>) {
|
2022-08-28 07:19:15 +00:00
|
|
|
let providerManager: ProviderManager = .shared
|
2022-04-12 13:09:14 +00:00
|
|
|
|
|
|
|
self.providerManager = providerManager
|
|
|
|
self.currentProfile = currentProfile
|
|
|
|
self.isEditable = isEditable
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-17 19:44:18 +00:00
|
|
|
_selectedServer = currentProfile.selectedServerBinding(providerManager: providerManager, isPresented: isPresented)
|
|
|
|
_favoriteLocationIds = currentProfile.providerFavoriteLocationIdsBinding
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
var body: some View {
|
|
|
|
debugChanges()
|
|
|
|
return Group {
|
2022-04-19 06:56:10 +00:00
|
|
|
if isProviderProfileAvailable {
|
2022-04-12 13:09:14 +00:00
|
|
|
mainView
|
|
|
|
} else {
|
|
|
|
EmptyView()
|
|
|
|
}
|
2022-04-21 13:56:26 +00:00
|
|
|
}.toolbar {
|
2023-03-25 16:05:53 +00:00
|
|
|
Button {
|
|
|
|
withAnimation {
|
|
|
|
isShowingFavorites.toggle()
|
2022-04-21 13:56:26 +00:00
|
|
|
}
|
2023-03-25 16:05:53 +00:00
|
|
|
} label: {
|
|
|
|
themeFavoritesImage(isShowingFavorites).asSystemImage
|
2022-04-21 13:56:26 +00:00
|
|
|
}
|
2022-04-12 13:09:14 +00:00
|
|
|
}.navigationTitle(L10n.Provider.Location.title)
|
|
|
|
}
|
2023-07-03 14:54:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
extension ProviderLocationView {
|
|
|
|
struct LocationRow: View {
|
|
|
|
let location: ProviderLocation
|
|
|
|
|
|
|
|
let selectedLocationId: String?
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
HStack {
|
|
|
|
themeAssetsCountryImage(location.countryCode).asAssetImage
|
|
|
|
VStack {
|
2023-07-05 15:18:33 +00:00
|
|
|
if let singleServer = location.onlyServer,
|
|
|
|
let shortServerDescription = singleServer.localizedDescription(optionalStyle: .short) {
|
|
|
|
|
|
|
|
Text(location.localizedDescription(style: .country))
|
2023-07-03 14:54:43 +00:00
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
2023-07-05 15:18:33 +00:00
|
|
|
Text(shortServerDescription)
|
2023-07-03 14:54:43 +00:00
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
} else {
|
2023-07-05 15:18:33 +00:00
|
|
|
Text(location.localizedDescription(style: .country))
|
2023-07-03 14:54:43 +00:00
|
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
|
|
|
}
|
|
|
|
}.withTrailingCheckmark(when: location.id == selectedLocationId)
|
|
|
|
}.frame(height: 60)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ServerListView: View {
|
|
|
|
@ObservedObject private var providerManager: ProviderManager
|
|
|
|
|
|
|
|
private let location: ProviderLocation
|
|
|
|
|
|
|
|
@Binding private var selectedServer: ProviderServer?
|
|
|
|
|
|
|
|
init(location: ProviderLocation, selectedServer: Binding<ProviderServer?>) {
|
|
|
|
providerManager = .shared
|
|
|
|
self.location = location
|
|
|
|
_selectedServer = selectedServer
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
var body: some View {
|
|
|
|
ScrollViewReader { scrollProxy in
|
|
|
|
List {
|
|
|
|
ForEach(servers) { server in
|
2023-07-05 15:18:33 +00:00
|
|
|
Button(server.localizedDescription(style: .shortWithDefault)) {
|
2023-07-03 14:54:43 +00:00
|
|
|
selectedServer = server
|
|
|
|
}.withTrailingCheckmark(when: server.id == selectedServer?.id)
|
|
|
|
}
|
|
|
|
}.onAppear {
|
|
|
|
scrollToSelectedServer(scrollProxy)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: -
|
|
|
|
|
|
|
|
private extension ProviderLocationView {
|
|
|
|
var providerName: ProviderName {
|
|
|
|
guard let name = currentProfile.value.header.providerName else {
|
|
|
|
assertionFailure("Not a provider")
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
|
|
|
|
var vpnProtocol: VPNProtocolType {
|
|
|
|
currentProfile.value.currentVPNProtocol
|
|
|
|
}
|
|
|
|
|
|
|
|
var mainView: some View {
|
2022-04-26 19:39:21 +00:00
|
|
|
// FIXME: layout, restore ScrollViewReader, but content inside it is not re-rendered on isShowingFavorites
|
2022-04-19 07:12:50 +00:00
|
|
|
// ScrollViewReader { scrollProxy in
|
2023-07-03 14:54:43 +00:00
|
|
|
List {
|
|
|
|
if !isShowingEmptyFavorites {
|
|
|
|
categoriesView
|
|
|
|
} else {
|
|
|
|
emptyFavoritesSection
|
|
|
|
}
|
2022-04-19 07:12:50 +00:00
|
|
|
// }.onAppear {
|
|
|
|
// scrollToSelectedLocation(scrollProxy)
|
2023-07-03 14:54:43 +00:00
|
|
|
}
|
2022-04-19 07:12:50 +00:00
|
|
|
// }
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
var categoriesView: some View {
|
2022-04-12 13:09:14 +00:00
|
|
|
ForEach(categories, content: categorySection)
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
func categorySection(_ category: ProviderCategory) -> some View {
|
2022-05-01 17:43:33 +00:00
|
|
|
Section {
|
2022-04-12 13:09:14 +00:00
|
|
|
ForEach(filteredLocations(for: category)) { location in
|
2023-03-25 16:05:53 +00:00
|
|
|
if isEditable {
|
2022-04-12 13:09:14 +00:00
|
|
|
locationRow(location)
|
2023-12-16 19:58:54 +00:00
|
|
|
#if !os(tvOS)
|
2022-04-12 13:09:14 +00:00
|
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
|
|
favoriteActions(location)
|
|
|
|
}
|
2023-12-16 19:58:54 +00:00
|
|
|
#endif
|
2022-04-12 13:09:14 +00:00
|
|
|
} else {
|
|
|
|
locationRow(location)
|
|
|
|
}
|
|
|
|
}
|
2022-05-01 17:43:33 +00:00
|
|
|
} header: {
|
|
|
|
!category.name.isEmpty ? Text(category.name) : nil
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
@ViewBuilder
|
2023-07-03 14:54:43 +00:00
|
|
|
func locationRow(_ location: ProviderLocation) -> some View {
|
2022-04-12 13:09:14 +00:00
|
|
|
if let onlyServer = location.onlyServer {
|
|
|
|
singleServerRow(location, onlyServer)
|
2023-03-19 07:19:32 +00:00
|
|
|
} else if profile.providerRandomizesServer ?? false {
|
|
|
|
singleServerRow(location, nil)
|
2022-04-12 13:09:14 +00:00
|
|
|
} else {
|
|
|
|
multipleServersRow(location)
|
|
|
|
}
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
func multipleServersRow(_ location: ProviderLocation) -> some View {
|
2022-04-12 13:09:14 +00:00
|
|
|
NavigationLink(destination: {
|
|
|
|
ServerListView(
|
|
|
|
location: location,
|
|
|
|
selectedServer: $selectedServer
|
2023-07-05 15:18:33 +00:00
|
|
|
).navigationTitle(location.localizedDescription(style: .country))
|
2022-04-12 13:09:14 +00:00
|
|
|
}, label: {
|
|
|
|
LocationRow(
|
|
|
|
location: location,
|
|
|
|
selectedLocationId: selectedServer?.locationId
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
func singleServerRow(_ location: ProviderLocation, _ server: ProviderServer?) -> some View {
|
2022-04-12 13:09:14 +00:00
|
|
|
Button {
|
2023-03-19 07:19:32 +00:00
|
|
|
selectedServer = server ?? location.servers?.randomElement()
|
2022-04-12 13:09:14 +00:00
|
|
|
} label: {
|
|
|
|
LocationRow(
|
|
|
|
location: location,
|
|
|
|
selectedLocationId: selectedServer?.locationId
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
func favoriteActions(_ location: ProviderLocation) -> some View {
|
2022-04-12 13:09:14 +00:00
|
|
|
Button {
|
|
|
|
withAnimation {
|
|
|
|
toggleFavoriteLocation(location)
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
themeFavoriteActionImage(!isFavoriteLocation(location)).asSystemImage
|
2022-04-23 09:42:26 +00:00
|
|
|
}.themePrimaryTintStyle()
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
2023-07-03 14:54:43 +00:00
|
|
|
|
|
|
|
var emptyFavoritesSection: some View {
|
|
|
|
Section {
|
|
|
|
} footer: {
|
|
|
|
Text(L10n.Provider.Location.Sections.EmptyFavorites.footer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var isShowingEmptyFavorites: Bool {
|
|
|
|
guard isShowingFavorites else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return favoriteLocationIds?.isEmpty ?? true
|
|
|
|
}
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
private extension ProviderLocationView {
|
|
|
|
func server(withId serverId: String) -> ProviderServer? {
|
2022-04-12 13:09:14 +00:00
|
|
|
providerManager.server(withId: serverId)
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
var categories: [ProviderCategory] {
|
2022-04-12 13:09:14 +00:00
|
|
|
providerManager.categories(providerName, vpnProtocol: vpnProtocol)
|
|
|
|
.filter {
|
|
|
|
!filteredLocations(for: $0).isEmpty
|
|
|
|
}.sorted()
|
|
|
|
}
|
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
func filteredLocations(for category: ProviderCategory) -> [ProviderLocation] {
|
2022-04-12 13:09:14 +00:00
|
|
|
let locations: [ProviderLocation]
|
|
|
|
if isShowingFavorites {
|
|
|
|
locations = category.locations.filter {
|
|
|
|
favoriteLocationIds?.contains($0.id) ?? false
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
locations = category.locations
|
|
|
|
}
|
|
|
|
return locations.sorted()
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
func isFavoriteLocation(_ location: ProviderLocation) -> Bool {
|
2022-09-04 18:09:31 +00:00
|
|
|
favoriteLocationIds?.contains(location.id) ?? false
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
2023-07-03 14:54:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private extension ProviderLocationView.ServerListView {
|
|
|
|
var servers: [ProviderServer] {
|
|
|
|
providerManager.servers(forLocation: location).sorted()
|
|
|
|
}
|
|
|
|
}
|
2023-03-17 20:55:47 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
// MARK: -
|
|
|
|
|
|
|
|
private extension ProviderLocationView {
|
|
|
|
func toggleFavoriteLocation(_ location: ProviderLocation) {
|
2022-04-12 13:09:14 +00:00
|
|
|
if !isFavoriteLocation(location) {
|
|
|
|
if favoriteLocationIds == nil {
|
|
|
|
favoriteLocationIds = [location.id]
|
|
|
|
} else {
|
|
|
|
favoriteLocationIds?.insert(location.id)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
favoriteLocationIds?.remove(location.id)
|
|
|
|
}
|
|
|
|
// may trigger view updates?
|
|
|
|
// pp_log.debug("New favorite locations: \(favoriteLocationIds ?? [])")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
private extension ProviderLocationView {
|
|
|
|
func scrollToSelectedLocation(_ proxy: ScrollViewProxy) {
|
2022-04-21 14:03:12 +00:00
|
|
|
proxy.maybeScrollTo(selectedServer?.locationId)
|
|
|
|
}
|
|
|
|
}
|
2022-04-12 13:09:14 +00:00
|
|
|
|
2023-07-03 14:54:43 +00:00
|
|
|
private extension ProviderLocationView.ServerListView {
|
|
|
|
func scrollToSelectedServer(_ proxy: ScrollViewProxy) {
|
2022-04-19 07:12:50 +00:00
|
|
|
proxy.maybeScrollTo(selectedServer?.id)
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
|
|
|
}
|
2023-07-17 19:44:18 +00:00
|
|
|
|
|
|
|
// MARK: - Bindings
|
|
|
|
|
|
|
|
private extension ObservableProfile {
|
2023-09-10 18:36:52 +00:00
|
|
|
|
|
|
|
@MainActor
|
2023-07-17 19:44:18 +00:00
|
|
|
func selectedServerBinding(providerManager: ProviderManager, isPresented: Binding<Bool>) -> Binding<ProviderServer?> {
|
|
|
|
.init {
|
|
|
|
guard let serverId = self.value.providerServerId else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return providerManager.server(withId: serverId)
|
|
|
|
} set: {
|
|
|
|
// user never selects a nil server
|
|
|
|
guard let server = $0 else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self.value.setProviderServer(server)
|
|
|
|
isPresented.wrappedValue = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var providerFavoriteLocationIdsBinding: Binding<Set<String>?> {
|
|
|
|
.init {
|
|
|
|
self.value.providerFavoriteLocationIds
|
|
|
|
} set: {
|
|
|
|
self.value.providerFavoriteLocationIds = $0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|