diff --git a/Passepartout/App/Platforms/App+macOS.swift b/Passepartout/App/Platforms/App+macOS.swift index 0657eac6..bc2044ce 100644 --- a/Passepartout/App/Platforms/App+macOS.swift +++ b/Passepartout/App/Platforms/App+macOS.swift @@ -83,10 +83,14 @@ extension PassepartoutApp { .withEnvironment(from: context, theme: theme) } MenuBarExtra { - AppMenu() - .withEnvironment(from: context, theme: theme) + AppMenu( + profileManager: context.profileManager, + profileProcessor: context.profileProcessor, + tunnel: context.tunnel + ) + .withEnvironment(from: context, theme: theme) } label: { - AppMenuImage(connectionObserver: context.connectionObserver) + AppMenuImage(tunnel: context.tunnel) .environmentObject(theme) } } diff --git a/Passepartout/Library/Sources/AppUI/Business/AppContext.swift b/Passepartout/Library/Sources/AppUI/Business/AppContext.swift index 735e1fb6..75f858fb 100644 --- a/Passepartout/Library/Sources/AppUI/Business/AppContext.swift +++ b/Passepartout/Library/Sources/AppUI/Business/AppContext.swift @@ -38,12 +38,10 @@ public final class AppContext: ObservableObject { public let profileProcessor: ProfileProcessor - public let tunnel: Tunnel + public let tunnel: ExtendedTunnel public let tunnelEnvironment: TunnelEnvironment - public let connectionObserver: ConnectionObserver - public let registry: Registry public let providerManager: ProviderManager @@ -65,9 +63,8 @@ public final class AppContext: ObservableObject { self.iapManager = iapManager self.profileManager = profileManager self.profileProcessor = profileProcessor - self.tunnel = tunnel self.tunnelEnvironment = tunnelEnvironment - connectionObserver = ConnectionObserver( + self.tunnel = ExtendedTunnel( tunnel: tunnel, environment: tunnelEnvironment, interval: constants.tunnel.refreshInterval @@ -79,7 +76,7 @@ public final class AppContext: ObservableObject { Task { await iapManager.reloadReceipt() - connectionObserver.observeObjects() + self.tunnel.observeObjects() profileManager.observeObjects() observeObjects() } diff --git a/Passepartout/Library/Sources/AppUI/Business/ConnectionObserver.swift b/Passepartout/Library/Sources/AppUI/Business/ExtendedTunnel.swift similarity index 59% rename from Passepartout/Library/Sources/AppUI/Business/ConnectionObserver.swift rename to Passepartout/Library/Sources/AppUI/Business/ExtendedTunnel.swift index 32c02cc3..8f3051f5 100644 --- a/Passepartout/Library/Sources/AppUI/Business/ConnectionObserver.swift +++ b/Passepartout/Library/Sources/AppUI/Business/ExtendedTunnel.swift @@ -1,5 +1,5 @@ // -// ConnectionObserver.swift +// ExtendedTunnel.swift // Passepartout // // Created by Davide De Rosa on 9/7/24. @@ -29,8 +29,8 @@ import Foundation import PassepartoutKit @MainActor -public final class ConnectionObserver: ObservableObject { - public let tunnel: Tunnel +public final class ExtendedTunnel: ObservableObject { + private let tunnel: Tunnel private let environment: TunnelEnvironment @@ -40,14 +40,10 @@ public final class ConnectionObserver: ObservableObject { environment.environmentValue(forKey: key) } - public var connectionStatus: ConnectionStatus? { - value(forKey: TunnelEnvironmentKeys.connectionStatus) - } - @Published public private(set) var lastErrorCode: PassepartoutError.Code? { didSet { - pp_log(.app, .info, "ConnectionObserver.lastErrorCode -> \(lastErrorCode?.rawValue ?? "nil")") + pp_log(.app, .info, "ExtendedTunnel.lastErrorCode -> \(lastErrorCode?.rawValue ?? "nil")") } } @@ -103,3 +99,63 @@ public final class ConnectionObserver: ObservableObject { .store(in: &subscriptions) } } + +extension ExtendedTunnel { + public var status: TunnelStatus { + tunnel.status + } + + public var connectionStatus: TunnelStatus { + var status = tunnel.status + if status == .active, let environmentConnectionStatus { + if environmentConnectionStatus == .connected { + status = .active + } else { + status = .activating + } + } + return status + } + + private var environmentConnectionStatus: ConnectionStatus? { + value(forKey: TunnelEnvironmentKeys.connectionStatus) + } +} + +extension ExtendedTunnel { + public var currentProfile: TunnelCurrentProfile? { + tunnel.currentProfile + } + + public func prepare(purge: Bool) async throws { + try await tunnel.prepare(purge: purge) + } + + public func install(_ profile: Profile, processor: ProfileProcessor) async throws { + let newProfile = try processor.processed(profile) + try await tunnel.install(newProfile, connect: false, title: processor.title) + } + + public func connect(with profile: Profile, processor: ProfileProcessor) async throws { + let newProfile = try processor.processed(profile) + try await tunnel.install(newProfile, connect: true, title: processor.title) + } + + public func disconnect() async throws { + try await tunnel.disconnect() + } + + public func currentLog(parameters: Constants.Log) async -> [String] { + let output = try? await tunnel.sendMessage(.localLog( + sinceLast: parameters.sinceLast, + maxLevel: parameters.maxLevel + )) + switch output { + case .debugLog(let log): + return log.lines.map(parameters.formatter.formattedLine) + + default: + return [] + } + } +} diff --git a/Passepartout/Library/Sources/AppUIMain/Business/InteractiveManager.swift b/Passepartout/Library/Sources/AppUI/Business/InteractiveManager.swift similarity index 77% rename from Passepartout/Library/Sources/AppUIMain/Business/InteractiveManager.swift rename to Passepartout/Library/Sources/AppUI/Business/InteractiveManager.swift index 481d12c1..b8ab279e 100644 --- a/Passepartout/Library/Sources/AppUIMain/Business/InteractiveManager.swift +++ b/Passepartout/Library/Sources/AppUI/Business/InteractiveManager.swift @@ -27,23 +27,26 @@ import Foundation import PassepartoutKit @MainActor -final class InteractiveManager: ObservableObject { - typealias CompletionBlock = (Profile) async throws -> Void +public final class InteractiveManager: ObservableObject { + public typealias CompletionBlock = (Profile) async throws -> Void @Published - var isPresented = false + public var isPresented = false - private(set) var editor = ProfileEditor() + public private(set) var editor = ProfileEditor() private var onComplete: CompletionBlock? - func present(with profile: Profile, onComplete: CompletionBlock?) { + public init() { + } + + public func present(with profile: Profile, onComplete: CompletionBlock?) { editor = ProfileEditor(profile: profile) self.onComplete = onComplete isPresented = true } - func complete() async throws { + public func complete() async throws { isPresented = false let newProfile = try editor.build() try await onComplete?(newProfile) diff --git a/Passepartout/Library/Sources/AppUI/Extensions/Tunnel+Extensions.swift b/Passepartout/Library/Sources/AppUI/Extensions/Tunnel+Extensions.swift deleted file mode 100644 index 732bc20c..00000000 --- a/Passepartout/Library/Sources/AppUI/Extensions/Tunnel+Extensions.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Tunnel+Extensions.swift -// Passepartout -// -// Created by Davide De Rosa on 8/11/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 . -// - -import CommonLibrary -import Foundation -import PassepartoutKit - -@MainActor -extension Tunnel { - public func install(_ profile: Profile, processor: ProfileProcessor) async throws { - let newProfile = try processor.processed(profile) - try await install(newProfile, connect: false, title: processor.title) - } - - public func connect(with profile: Profile, processor: ProfileProcessor) async throws { - let newProfile = try processor.processed(profile) - try await install(newProfile, connect: true, title: processor.title) - } - - public func currentLog(parameters: Constants.Log) async -> [String] { - let output = try? await sendMessage(.localLog( - sinceLast: parameters.sinceLast, - maxLevel: parameters.maxLevel - )) - switch output { - case .debugLog(let log): - return log.lines.map(parameters.formatter.formattedLine) - - default: - return [] - } - } -} diff --git a/Passepartout/Library/Sources/AppUI/Extensions/View+Environment.swift b/Passepartout/Library/Sources/AppUI/Extensions/View+Environment.swift index dd3ce916..240cb12f 100644 --- a/Passepartout/Library/Sources/AppUI/Extensions/View+Environment.swift +++ b/Passepartout/Library/Sources/AppUI/Extensions/View+Environment.swift @@ -30,12 +30,9 @@ import SwiftUI extension View { public func withEnvironment(from context: AppContext, theme: Theme) -> some View { environmentObject(theme) - .environmentObject(context.connectionObserver) .environmentObject(context.iapManager) - .environmentObject(context.profileManager) .environmentObject(context.profileProcessor) .environmentObject(context.providerManager) - .environmentObject(context.tunnel) } public func withMockEnvironment() -> some View { diff --git a/Passepartout/Library/Sources/AppUI/Mock/Mock.swift b/Passepartout/Library/Sources/AppUI/Mock/Mock.swift index 1cfb58c4..f0033b12 100644 --- a/Passepartout/Library/Sources/AppUI/Mock/Mock.swift +++ b/Passepartout/Library/Sources/AppUI/Mock/Mock.swift @@ -88,18 +88,12 @@ extension ProfileProcessor { } } -extension Tunnel { - public static var mock: Tunnel { +extension ExtendedTunnel { + public static var mock: ExtendedTunnel { AppContext.mock.tunnel } } -extension ConnectionObserver { - public static var mock: ConnectionObserver { - AppContext.mock.connectionObserver - } -} - extension ProviderManager { public static var mock: ProviderManager { AppContext.mock.providerManager diff --git a/Passepartout/Library/Sources/AppUI/Protocols/AppCoordinatorConforming.swift b/Passepartout/Library/Sources/AppUI/Protocols/AppCoordinatorConforming.swift index b4629ba0..05b8201e 100644 --- a/Passepartout/Library/Sources/AppUI/Protocols/AppCoordinatorConforming.swift +++ b/Passepartout/Library/Sources/AppUI/Protocols/AppCoordinatorConforming.swift @@ -30,7 +30,7 @@ import PassepartoutKit public protocol AppCoordinatorConforming { init( profileManager: ProfileManager, - tunnel: Tunnel, + tunnel: ExtendedTunnel, registry: Registry ) } diff --git a/Passepartout/Library/Sources/AppUI/Protocols/TunnelContextProviding.swift b/Passepartout/Library/Sources/AppUI/Protocols/TunnelContextProviding.swift deleted file mode 100644 index cffe0f16..00000000 --- a/Passepartout/Library/Sources/AppUI/Protocols/TunnelContextProviding.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// TunnelContextProviding.swift -// Passepartout -// -// Created by Davide De Rosa on 9/5/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 . -// - -import Foundation -import PassepartoutKit - -public protocol TunnelContextProviding { - var connectionObserver: ConnectionObserver { get } -} - -@MainActor -extension TunnelContextProviding { - public var tunnelConnectionStatus: TunnelStatus { - var status = connectionObserver.tunnel.status - if status == .active, let connectionStatus = connectionObserver.connectionStatus { - if connectionStatus == .connected { - status = .active - } else { - status = .activating - } - } - return status - } -} diff --git a/Passepartout/Library/Sources/AppUI/Protocols/TunnelInstallationProviding.swift b/Passepartout/Library/Sources/AppUI/Protocols/TunnelInstallationProviding.swift index 7fde139e..746a7328 100644 --- a/Passepartout/Library/Sources/AppUI/Protocols/TunnelInstallationProviding.swift +++ b/Passepartout/Library/Sources/AppUI/Protocols/TunnelInstallationProviding.swift @@ -30,7 +30,7 @@ import PassepartoutKit public protocol TunnelInstallationProviding { var profileManager: ProfileManager { get } - var tunnel: Tunnel { get } + var tunnel: ExtendedTunnel { get } } @MainActor diff --git a/Passepartout/Library/Sources/AppUIMain/UI/ConnectionStatusView.swift b/Passepartout/Library/Sources/AppUI/UI/ConnectionStatusView.swift similarity index 78% rename from Passepartout/Library/Sources/AppUIMain/UI/ConnectionStatusView.swift rename to Passepartout/Library/Sources/AppUI/UI/ConnectionStatusView.swift index 095c5cbe..d163f444 100644 --- a/Passepartout/Library/Sources/AppUIMain/UI/ConnectionStatusView.swift +++ b/Passepartout/Library/Sources/AppUI/UI/ConnectionStatusView.swift @@ -27,32 +27,34 @@ import Foundation import PassepartoutKit import SwiftUI -struct ConnectionStatusView: View, TunnelContextProviding, ThemeProviding { +public struct ConnectionStatusView: View, ThemeProviding { @EnvironmentObject - var theme: Theme - - @EnvironmentObject - var connectionObserver: ConnectionObserver + public var theme: Theme @ObservedObject - var tunnel: Tunnel + private var tunnel: ExtendedTunnel - var body: some View { + public init(tunnel: ExtendedTunnel) { + self.tunnel = tunnel + } + + public var body: some View { Text(statusDescription) - .foregroundStyle(tunnelStatusColor) + .font(.headline) + .foregroundStyle(tunnel.statusColor(theme)) } } private extension ConnectionStatusView { var statusDescription: String { - if let lastErrorCode = connectionObserver.lastErrorCode { + if let lastErrorCode = tunnel.lastErrorCode { return lastErrorCode.localizedDescription } - let status = tunnelConnectionStatus + let status = tunnel.connectionStatus switch status { case .active: - if let dataCount = connectionObserver.dataCount { + if let dataCount = tunnel.dataCount { let down = dataCount.received.descriptionAsDataUnit let up = dataCount.sent.descriptionAsDataUnit return "↓\(down) ↑\(up)" @@ -75,7 +77,7 @@ private extension ConnectionStatusView { #Preview("Connected") { ConnectionStatusView(tunnel: .mock) .task { - try? await Tunnel.mock.connect(with: .mock, processor: .mock) + try? await ExtendedTunnel.mock.connect(with: .mock, processor: .mock) } .frame(width: 100, height: 100) .withMockEnvironment() @@ -94,7 +96,7 @@ private extension ConnectionStatusView { } return ConnectionStatusView(tunnel: .mock) .task { - try? await Tunnel.mock.connect(with: profile, processor: .mock) + try? await ExtendedTunnel.mock.connect(with: profile, processor: .mock) } .frame(width: 100, height: 100) .withMockEnvironment() diff --git a/Passepartout/Library/Sources/AppUIMain/UI/TunnelContextProviding+Theme.swift b/Passepartout/Library/Sources/AppUI/UI/ExtendedTunnel+Theme.swift similarity index 81% rename from Passepartout/Library/Sources/AppUIMain/UI/TunnelContextProviding+Theme.swift rename to Passepartout/Library/Sources/AppUI/UI/ExtendedTunnel+Theme.swift index d6a5535c..dd565155 100644 --- a/Passepartout/Library/Sources/AppUIMain/UI/TunnelContextProviding+Theme.swift +++ b/Passepartout/Library/Sources/AppUI/UI/ExtendedTunnel+Theme.swift @@ -1,5 +1,5 @@ // -// TunnelContextProviding+Theme.swift +// ExtendedTunnel+Theme.swift // Passepartout // // Created by Davide De Rosa on 9/6/24. @@ -26,11 +26,12 @@ import PassepartoutKit import SwiftUI -@MainActor -extension TunnelContextProviding where Self: ThemeProviding { - var tunnelStatusColor: Color { - if connectionObserver.lastErrorCode != nil { - switch connectionObserver.tunnel.status { +extension ExtendedTunnel { + + @MainActor + public func statusColor(_ theme: Theme) -> Color { + if lastErrorCode != nil { + switch status { case .inactive: return theme.inactiveColor @@ -38,7 +39,7 @@ extension TunnelContextProviding where Self: ThemeProviding { return theme.errorColor } } - switch tunnelConnectionStatus { + switch connectionStatus { case .active: return theme.activeColor diff --git a/Passepartout/Library/Sources/AppUIMain/UI/TunnelToggleButton.swift b/Passepartout/Library/Sources/AppUI/UI/TunnelToggleButton.swift similarity index 76% rename from Passepartout/Library/Sources/AppUIMain/UI/TunnelToggleButton.swift rename to Passepartout/Library/Sources/AppUI/UI/TunnelToggleButton.swift index 64c2333d..93df8505 100644 --- a/Passepartout/Library/Sources/AppUIMain/UI/TunnelToggleButton.swift +++ b/Passepartout/Library/Sources/AppUI/UI/TunnelToggleButton.swift @@ -27,18 +27,15 @@ import PassepartoutKit import SwiftUI import UtilsLibrary -struct TunnelToggleButton