passepartout-apple/Passepartout/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift

263 lines
7.4 KiB
Swift
Raw Normal View History

2024-09-23 13:02:26 +00:00
//
// OnDemandView.swift
// Passepartout
//
// Created by Davide De Rosa on 2/23/24.
// Copyright (c) 2024 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 AppLibrary
import CommonUtils
2024-09-23 13:02:26 +00:00
import PassepartoutKit
import SwiftUI
struct OnDemandView: View, ModuleDraftEditing {
2024-09-23 13:02:26 +00:00
@EnvironmentObject
private var theme: Theme
@EnvironmentObject
private var iapManager: IAPManager
2024-09-23 13:02:26 +00:00
@ObservedObject
var editor: ProfileEditor
2024-09-23 13:02:26 +00:00
let module: OnDemandModule.Builder
2024-09-23 13:02:26 +00:00
private let wifi: Wifi
2024-09-23 13:02:26 +00:00
@State
private var paywallReason: PaywallReason?
2024-09-23 13:02:26 +00:00
init(
editor: ProfileEditor,
module: OnDemandModule.Builder,
2024-09-23 13:02:26 +00:00
observer: WifiObserver? = nil
) {
self.editor = editor
self.module = module
2024-09-23 13:02:26 +00:00
wifi = Wifi(observer: observer ?? CoreLocationWifiObserver())
}
var body: some View {
Group {
enabledSection
restrictedArea
2024-09-23 13:02:26 +00:00
}
.moduleView(editor: editor, draft: draft.wrappedValue)
.modifier(PaywallModifier(reason: $paywallReason))
2024-09-23 13:02:26 +00:00
}
}
private extension OnDemandView {
static let allPolicies: [OnDemandModule.Policy] = [
.any,
.excluding,
.including
]
var enabledSection: some View {
Section {
Toggle(Strings.Global.enabled, isOn: draft.isEnabled)
2024-09-23 13:02:26 +00:00
}
}
@ViewBuilder
var restrictedArea: some View {
switch iapManager.paywallReason(forFeature: .onDemand) {
case .purchase(let appFeature):
Button(Strings.Modules.OnDemand.purchase) {
paywallReason = .purchase(appFeature)
}
case .restricted:
EmptyView()
default:
if draft.wrappedValue.isEnabled {
policySection
if draft.wrappedValue.policy != .any {
networkSection
wifiSection
}
}
}
}
2024-09-23 13:02:26 +00:00
var policySection: some View {
Picker(Strings.Modules.OnDemand.policy, selection: draft.policy) {
2024-09-23 13:02:26 +00:00
ForEach(Self.allPolicies, id: \.self) {
Text($0.localizedDescription)
}
}
.themeSection(footer: policyFooterDescription)
2024-09-23 13:02:26 +00:00
}
var policyFooterDescription: String {
guard draft.wrappedValue.isEnabled else {
2024-09-23 13:02:26 +00:00
return "" // better animation than removing footer completely
}
let suffix: String
switch draft.wrappedValue.policy {
2024-09-23 13:02:26 +00:00
case .any:
suffix = Strings.Modules.OnDemand.Policy.Footer.any
case .including, .excluding:
if draft.wrappedValue.policy == .including {
2024-09-23 13:02:26 +00:00
suffix = Strings.Modules.OnDemand.Policy.Footer.including
} else {
suffix = Strings.Modules.OnDemand.Policy.Footer.excluding
}
}
return Strings.Modules.OnDemand.Policy.footer(suffix)
}
var networkSection: some View {
Group {
2024-09-23 13:02:26 +00:00
if Utils.hasCellularData() {
Toggle(Strings.Modules.OnDemand.mobile, isOn: draft.withMobileNetwork)
2024-09-23 13:02:26 +00:00
} else if Utils.hasEthernet() {
Toggle(Strings.Modules.OnDemand.ethernet, isOn: draft.withEthernetNetwork)
2024-09-23 13:02:26 +00:00
}
}
.themeSection(header: Strings.Global.networks)
2024-09-23 13:02:26 +00:00
}
var wifiSection: some View {
theme.listSection(
Strings.Unlocalized.wifi,
addTitle: Strings.Modules.OnDemand.Ssid.add,
originalItems: allSSIDs,
emptyValue: {
do {
return try await wifi.currentSSID()
} catch {
return ""
}
},
itemLabel: { isEditing, binding in
if isEditing {
Text(binding.wrappedValue)
} else {
HStack {
ThemeTextField("", text: binding, placeholder: Strings.Placeholders.OnDemand.ssid)
.frame(maxWidth: .infinity)
.themeManualInput()
Spacer()
Toggle("", isOn: isSSIDOn(binding.wrappedValue))
}
.labelsHidden()
}
}
)
}
}
private extension OnDemandView {
var allSSIDs: Binding<[String]> {
.init {
Array(draft.wrappedValue.withSSIDs.keys)
2024-09-23 13:02:26 +00:00
} set: { newValue in
draft.wrappedValue.withSSIDs.forEach {
2024-09-23 13:02:26 +00:00
guard newValue.contains($0.key) else {
draft.wrappedValue.withSSIDs.removeValue(forKey: $0.key)
2024-09-23 13:02:26 +00:00
return
}
}
newValue.forEach {
guard draft.wrappedValue.withSSIDs[$0] == nil else {
2024-09-23 13:02:26 +00:00
return
}
draft.wrappedValue.withSSIDs[$0] = false
2024-09-23 13:02:26 +00:00
}
}
}
var onSSIDs: Binding<Set<String>> {
.init {
Set(draft.wrappedValue.withSSIDs.filter {
2024-09-23 13:02:26 +00:00
$0.value
}.map(\.key))
} set: { newValue in
draft.wrappedValue.withSSIDs.forEach {
2024-09-23 13:02:26 +00:00
guard newValue.contains($0.key) else {
if draft.wrappedValue.withSSIDs[$0.key] != nil {
draft.wrappedValue.withSSIDs[$0.key] = false
2024-09-23 13:02:26 +00:00
} else {
draft.wrappedValue.withSSIDs.removeValue(forKey: $0.key)
2024-09-23 13:02:26 +00:00
}
return
}
}
newValue.forEach {
guard !(draft.wrappedValue.withSSIDs[$0] ?? false) else {
2024-09-23 13:02:26 +00:00
return
}
draft.wrappedValue.withSSIDs[$0] = true
2024-09-23 13:02:26 +00:00
}
}
}
func isSSIDOn(_ ssid: String) -> Binding<Bool> {
.init {
draft.wrappedValue.withSSIDs[ssid] ?? false
2024-09-23 13:02:26 +00:00
} set: {
draft.wrappedValue.withSSIDs[ssid] = $0
2024-09-23 13:02:26 +00:00
}
}
}
private extension OnDemandView {
func requestSSID(_ text: Binding<String>) {
Task { @MainActor in
let ssid = try await wifi.currentSSID()
if !draft.wrappedValue.withSSIDs.keys.contains(ssid) {
2024-09-23 13:02:26 +00:00
text.wrappedValue = ssid
}
}
}
}
// MARK: - Previews
#Preview {
var module = OnDemandModule.Builder()
module.policy = .excluding
module.withMobileNetwork = true
module.withSSIDs = [
"One": true,
"Two": false,
"Three": false
]
return module.preview {
OnDemandView(
editor: $0,
module: $1,
2024-09-23 13:02:26 +00:00
observer: MockWifi()
)
}
}
private class MockWifi: WifiObserver {
func currentSSID() async throws -> String {
""
}
}