// // IPView.swift // Passepartout // // Created by Davide De Rosa on 2/17/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 CommonUtils import PassepartoutKit import SwiftUI struct IPView: View, ModuleDraftEditing { @ObservedObject var editor: ProfileEditor let module: IPModule.Builder @State private var routePresentation: RoutePresentation? var body: some View { Group { ipSections(for: .v4) ipSections(for: .v6) interfaceSection } .moduleView(editor: editor, draft: draft.wrappedValue) .themeModal(item: $routePresentation, content: routeModal) } } private extension IPView { enum RoutePresentation: Identifiable { case included(Address.Family) case excluded(Address.Family) var id: String { switch self { case .included(let family): return "included.\(family)" case .excluded(let family): return "excluded.\(family)" } } var family: Address.Family { switch self { case .included(let family): return family case .excluded(let family): return family } } var localizedTitle: String { switch self { case .included: return Strings.Modules.Ip.Routes.include case .excluded: return Strings.Modules.Ip.Routes.exclude } } } @ViewBuilder func ipSections(for family: Address.Family) -> some View { let ip = binding(forSettingsIn: family) ForEach(Array(ip.wrappedValue.includedRoutes.enumerated()), id: \.offset) { item in row(forRoute: item.element) { ip.wrappedValue.removeIncluded(at: IndexSet(integer: item.offset)) } } .onDelete { ip.wrappedValue.removeIncluded(at: $0) } .asSectionWithHeader(family.localizedDescription) { Button(Strings.Modules.Ip.Routes.include) { routePresentation = .included(family) } } ForEach(Array(ip.wrappedValue.excludedRoutes.enumerated()), id: \.offset) { item in row(forRoute: item.element) { ip.wrappedValue.removeExcluded(at: IndexSet(integer: item.offset)) } } .onDelete { ip.wrappedValue.removeExcluded(at: $0) } .asSectionWithTrailingContent { Button(Strings.Modules.Ip.Routes.exclude) { routePresentation = .excluded(family) } } } func row(forRoute route: Route, removeAction: @escaping () -> Void) -> some View { ThemeRemovableItemRow(isEditing: true) { ThemeCopiableText(value: route.localizedDescription) { Text($0) } } removeAction: { removeAction() } } var interfaceSection: some View { Group { ThemeTextField( Strings.Unlocalized.mtu, text: Binding { draft.wrappedValue.mtu?.description ?? "" } set: { draft.wrappedValue.mtu = Int($0) }, placeholder: Strings.Unlocalized.Placeholders.mtu ) } .themeSection(header: Strings.Global.Nouns.interface) } } private extension IPView { func binding(forSettingsIn family: Address.Family) -> Binding<IPSettings> { switch family { case .v4: return Binding { draft.wrappedValue.ipv4 ?? IPSettings(subnet: nil) } set: { draft.wrappedValue.ipv4 = $0 } case .v6: return Binding { draft.wrappedValue.ipv6 ?? IPSettings(subnet: nil) } set: { draft.wrappedValue.ipv6 = $0 } } } func routeModal(item: RoutePresentation) -> some View { NavigationStack { RouteView(family: item.family) { route in defer { routePresentation = nil } guard let route else { return } switch item { case .included(let family): switch family { case .v4: if draft.wrappedValue.ipv4 == nil { draft.wrappedValue.ipv4 = IPSettings(subnet: nil) } draft.wrappedValue.ipv4?.include(route) case .v6: if draft.wrappedValue.ipv6 == nil { draft.wrappedValue.ipv6 = IPSettings(subnet: nil) } draft.wrappedValue.ipv6?.include(route) } case .excluded(let family): switch family { case .v4: if draft.wrappedValue.ipv4 == nil { draft.wrappedValue.ipv4 = IPSettings(subnet: nil) } draft.wrappedValue.ipv4?.exclude(route) case .v6: if draft.wrappedValue.ipv6 == nil { draft.wrappedValue.ipv6 = IPSettings(subnet: nil) } draft.wrappedValue.ipv6?.exclude(route) } } } .navigationTitle(item.localizedTitle) } } } #Preview { var module = IPModule.Builder() module.ipv4 = IPSettings(subnet: nil) .including(routes: [ .init(defaultWithGateway: .ip("1.2.3.4", .v4)), .init(.init(rawValue: "5.5.0.0/16"), .init(rawValue: "5.5.5.5")) ]) module.ipv6 = IPSettings(subnet: nil) .including(routes: [ .init(defaultWithGateway: .ip("fe80::1032:2a6b:fec:f49e", .v6)), .init(.init(rawValue: "fe80:1032:2a6b:fec::/24"), .init(rawValue: "fe80:1032:2a6b:fec::1")) ]) return module.preview() }