2022-04-12 13:09:14 +00:00
|
|
|
//
|
|
|
|
// ProviderLocationView.swift
|
|
|
|
// Passepartout
|
|
|
|
//
|
|
|
|
// Created by Davide De Rosa on 2/19/22.
|
|
|
|
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
|
|
|
//
|
|
|
|
// 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/>.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
import PassepartoutCore
|
|
|
|
|
2022-04-19 06:56:10 +00:00
|
|
|
struct ProviderLocationView: View, ProviderProfileAvailability {
|
|
|
|
@ObservedObject var providerManager: ProviderManager
|
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
@ObservedObject private var appManager: AppManager
|
|
|
|
|
|
|
|
@ObservedObject private var currentProfile: ObservableProfile
|
|
|
|
|
2022-04-19 06:56:10 +00:00
|
|
|
var profile: Profile {
|
|
|
|
currentProfile.value
|
|
|
|
}
|
2022-04-12 13:09:14 +00:00
|
|
|
|
2022-04-19 06:56:10 +00:00
|
|
|
private let isEditable: Bool
|
|
|
|
|
2022-04-12 13:09:14 +00:00
|
|
|
private var providerName: ProviderName {
|
|
|
|
guard let name = currentProfile.value.header.providerName else {
|
|
|
|
assertionFailure("Not a provider")
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
|
|
|
|
private var vpnProtocol: VPNProtocolType {
|
|
|
|
currentProfile.value.currentVPNProtocol
|
|
|
|
}
|
|
|
|
|
|
|
|
@Binding private var selectedServer: ProviderServer?
|
|
|
|
|
|
|
|
@Binding private var favoriteLocationIds: Set<String>?
|
|
|
|
|
|
|
|
@AppStorage(AppManager.DefaultKey.isShowingFavorites.rawValue) private var isShowingFavorites = false
|
|
|
|
|
|
|
|
private var isShowingEmptyFavorites: Bool {
|
|
|
|
guard isShowingFavorites else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return favoriteLocationIds?.isEmpty ?? true
|
|
|
|
}
|
|
|
|
|
|
|
|
// XXX: do not escape mutating 'self', use constant providerManager
|
|
|
|
init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding<Bool>) {
|
|
|
|
let providerManager: ProviderManager = .shared
|
|
|
|
|
|
|
|
appManager = .shared
|
|
|
|
self.providerManager = providerManager
|
|
|
|
self.currentProfile = currentProfile
|
|
|
|
self.isEditable = isEditable
|
|
|
|
|
|
|
|
_selectedServer = .init {
|
|
|
|
guard let serverId = currentProfile.value.providerServerId() else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return providerManager.server(withId: serverId)
|
|
|
|
} set: {
|
|
|
|
// user never selects a nil server
|
|
|
|
guard let server = $0 else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
currentProfile.value.setProviderServer(server)
|
|
|
|
isPresented.wrappedValue = false
|
|
|
|
}
|
|
|
|
_favoriteLocationIds = .init {
|
|
|
|
currentProfile.value.providerFavoriteLocationIds()
|
|
|
|
} set: {
|
|
|
|
currentProfile.value.setProviderFavoriteLocationIds($0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
if #available(iOS 15, *) {
|
|
|
|
Button {
|
|
|
|
withAnimation {
|
|
|
|
isShowingFavorites.toggle()
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
themeFavoritesImage(isShowingFavorites).asSystemImage
|
|
|
|
}
|
|
|
|
}
|
2022-04-12 13:09:14 +00:00
|
|
|
}.navigationTitle(L10n.Provider.Location.title)
|
|
|
|
}
|
|
|
|
|
|
|
|
private 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
|
2022-04-12 13:09:14 +00:00
|
|
|
List {
|
|
|
|
if !isShowingEmptyFavorites {
|
|
|
|
categoriesView
|
|
|
|
} else {
|
|
|
|
emptyFavoritesSection
|
|
|
|
}
|
2022-04-19 07:12:50 +00:00
|
|
|
// }.onAppear {
|
|
|
|
// scrollToSelectedLocation(scrollProxy)
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
2022-04-19 07:12:50 +00:00
|
|
|
// }
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private var categoriesView: some View {
|
|
|
|
ForEach(categories, content: categorySection)
|
|
|
|
}
|
|
|
|
|
|
|
|
private 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
|
2022-04-19 12:18:26 +00:00
|
|
|
if isEditable, #available(iOS 15, *) {
|
2022-04-12 13:09:14 +00:00
|
|
|
locationRow(location)
|
|
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
|
|
favoriteActions(location)
|
|
|
|
}
|
|
|
|
} 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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
private func locationRow(_ location: ProviderLocation) -> some View {
|
|
|
|
if let onlyServer = location.onlyServer {
|
|
|
|
singleServerRow(location, onlyServer)
|
|
|
|
} else {
|
|
|
|
multipleServersRow(location)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func multipleServersRow(_ location: ProviderLocation) -> some View {
|
|
|
|
NavigationLink(destination: {
|
|
|
|
ServerListView(
|
|
|
|
location: location,
|
|
|
|
selectedServer: $selectedServer
|
|
|
|
).navigationTitle(location.localizedCountry)
|
|
|
|
}, label: {
|
|
|
|
LocationRow(
|
|
|
|
location: location,
|
|
|
|
selectedLocationId: selectedServer?.locationId
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private func singleServerRow(_ location: ProviderLocation, _ server: ProviderServer) -> some View {
|
|
|
|
Button {
|
|
|
|
selectedServer = server
|
|
|
|
} label: {
|
|
|
|
LocationRow(
|
|
|
|
location: location,
|
|
|
|
selectedLocationId: selectedServer?.locationId
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var emptyFavoritesSection: some View {
|
2022-05-01 17:43:33 +00:00
|
|
|
Section {
|
|
|
|
} footer: {
|
|
|
|
Text(L10n.Provider.Location.Sections.EmptyFavorites.footer)
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-19 12:18:26 +00:00
|
|
|
@available(iOS 15, *)
|
2022-04-12 13:09:14 +00:00
|
|
|
private func favoriteActions(_ location: ProviderLocation) -> some View {
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ProviderLocationView {
|
|
|
|
private func server(withId serverId: String) -> ProviderServer? {
|
|
|
|
providerManager.server(withId: serverId)
|
|
|
|
}
|
|
|
|
|
|
|
|
private var categories: [ProviderCategory] {
|
|
|
|
providerManager.categories(providerName, vpnProtocol: vpnProtocol)
|
|
|
|
.filter {
|
|
|
|
!filteredLocations(for: $0).isEmpty
|
|
|
|
}.sorted()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func filteredLocations(for category: ProviderCategory) -> [ProviderLocation] {
|
|
|
|
let locations: [ProviderLocation]
|
|
|
|
if isShowingFavorites {
|
|
|
|
locations = category.locations.filter {
|
|
|
|
favoriteLocationIds?.contains($0.id) ?? false
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
locations = category.locations
|
|
|
|
}
|
|
|
|
return locations.sorted()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func isFavoriteLocation(_ location: ProviderLocation) -> Bool {
|
|
|
|
return favoriteLocationIds?.contains(location.id) ?? false
|
|
|
|
}
|
|
|
|
|
|
|
|
private func toggleFavoriteLocation(_ location: ProviderLocation) {
|
|
|
|
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 ?? [])")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ProviderLocationView {
|
|
|
|
struct LocationRow: View {
|
|
|
|
let location: ProviderLocation
|
|
|
|
|
|
|
|
let selectedLocationId: String?
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
HStack {
|
|
|
|
themeAssetsCountryImage(location.countryCode).asAssetImage
|
|
|
|
VStack {
|
|
|
|
if let singleServer = location.onlyServer, let _ = singleServer.details {
|
|
|
|
Text(location.localizedCountry)
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
Text(singleServer.localizedDetails.uppercased())
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
} else {
|
|
|
|
Text(location.localizedCountry)
|
|
|
|
.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
|
|
|
|
}
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
ScrollViewReader { scrollProxy in
|
|
|
|
List {
|
|
|
|
ForEach(servers) { server in
|
|
|
|
Button(server.localizedDetailsWithDefault) {
|
|
|
|
selectedServer = server
|
|
|
|
}.withTrailingCheckmark(when: server.id == selectedServer?.id)
|
|
|
|
}
|
|
|
|
}.onAppear {
|
|
|
|
scrollToSelectedServer(scrollProxy)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private var servers: [ProviderServer] {
|
|
|
|
return providerManager.servers(forLocation: location).sorted()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-21 14:03:12 +00:00
|
|
|
extension ProviderLocationView {
|
|
|
|
private func scrollToSelectedLocation(_ proxy: ScrollViewProxy) {
|
|
|
|
proxy.maybeScrollTo(selectedServer?.locationId)
|
|
|
|
}
|
|
|
|
}
|
2022-04-12 13:09:14 +00:00
|
|
|
|
|
|
|
extension ProviderLocationView.ServerListView {
|
|
|
|
private func scrollToSelectedServer(_ proxy: ScrollViewProxy) {
|
2022-04-19 07:12:50 +00:00
|
|
|
proxy.maybeScrollTo(selectedServer?.id)
|
2022-04-12 13:09:14 +00:00
|
|
|
}
|
|
|
|
}
|