Add SwiftLint phase (#262)

This commit is contained in:
Davide De Rosa 2023-03-17 21:55:47 +01:00 committed by GitHub
parent cecf64d871
commit f06f097f27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
227 changed files with 1399 additions and 1389 deletions

View File

@ -1047,6 +1047,7 @@
0E41BDA828671339006346B4 /* Embed Launcher */,
0E3152B7223F9EF500F61841 /* Embed Plugins */,
0EB2B14B2733FB6F007705AB /* Embed Foundation Extensions */,
0E1B5F5D29C5082700FE7D18 /* SwiftLint */,
);
buildRules = (
);
@ -1297,6 +1298,24 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0E1B5F5D29C5082700FE7D18 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = SwiftLint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
0EADDC7227F0677F0093E303 /* Copy Core Data codegen */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;

View File

@ -28,8 +28,8 @@ import UIKit
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
private let mac = MacBundle.shared
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
#if targetEnvironment(macCatalyst)
mac.configure()
mac.menu.install()

View File

@ -44,7 +44,7 @@ extension Constants {
Bundle.main.isTestFlight
}()
}
enum Plugins {
static let macBridgeName = "PassepartoutMac.bundle"
}
@ -92,9 +92,9 @@ extension Constants {
static let disableVPN = "DisableVPNIntent"
static let connectVPN = "ConnectVPNIntent"
static let moveToLocation = "MoveToLocationIntent"
static let trustCellularNetwork = "TrustCellularNetworkIntent"
static let trustCurrentNetwork = "TrustCurrentNetworkIntent"
@ -109,7 +109,7 @@ extension Constants {
enum Domain {
static let name = "passepartoutvpn.app"
}
enum Services {
static let version = "v5"
@ -120,38 +120,38 @@ extension Constants {
"https://www.facebook.com",
"https://www.instagram.com"
]
static let connectivityURL = URL(string: connectivityStrings.randomElement()!)!
static let connectivityTimeout: TimeInterval = 10.0
}
enum Persistence {
static let profilesContainerName = "Profiles"
static let providersContainerName = "Providers"
}
// milliseconds
enum RateLimit {
static let providerManager = 10000
static let vpnToggle = 500
}
enum Log {
enum App {
static let url = containerURL(filename: "App.log")
static let format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
}
enum Tunnel {
static let path = containerPath(filename: "Tunnel.log")
static let format = "$DHH:mm:ss$d - $M"
}
private static let parentPath = "Library/Caches"
static let level: SwiftyBeaver.Level = {
@ -160,9 +160,9 @@ extension Constants {
}
return .init(rawValue: levelNum) ?? .info
}()
static let maxBytes = 100000
static let refreshInterval: TimeInterval = 5.0
private static func containerURL(filename: String) -> URL {
@ -175,30 +175,30 @@ extension Constants {
"\(parentPath)/\(filename)"
}
}
enum URLs {
static let readme = Repos.apple.appendingPathComponent("blob/master/README.md")
static let changelog = Repos.apple.appendingPathComponent("blob/master/CHANGELOG.md")
static let filetypes: [UTType] = [.item]
static let website = URL(string: "https://\(Domain.name)")!
static let faq = website.appendingPathComponent("faq")
static let disclaimer = website.appendingPathComponent("disclaimer")
static let privacyPolicy = website.appendingPathComponent("privacy")
static let donate = website.appendingPathComponent("donate")
static let subreddit = URL(string: "https://www.reddit.com/r/passepartout")!
static let twitch = URL(string: "twitch://stream/keeshux")!
static let twitchFallback = URL(string: "https://twitch.tv/keeshux")!
static let githubSponsors = URL(string: "https://www.github.com/sponsors/passepartoutvpn")!
}
@ -206,31 +206,31 @@ extension Constants {
private static let githubRoot = URL(string: "https://github.com/passepartoutvpn/")!
private static let githubRawRoot = URL(string: "https://\(Domain.name)/")!
private static func github(repo: String) -> URL {
githubRoot.appendingPathComponent(repo)
}
private static func githubRaw(repo: String) -> URL {
githubRawRoot.appendingPathComponent(repo)
}
static let apple = github(repo: "passepartout-apple")
static let api = githubRaw(repo: "api")
}
// milliseconds
enum Delays {
static let scrolling = 100
// @available(*, deprecated, message: "file importer stops showing again after closing with swipe down")
static let xxxPresentFileImporter = 200
// @available(*, deprecated, message: "edited shortcut is outdated in delegate")
static let xxxReloadEditedShortcut = 200
}
enum Rating {
#if targetEnvironment(macCatalyst)
static let eventCount = 10

View File

@ -30,7 +30,7 @@ extension View {
var themeIdiom: UIUserInterfaceIdiom {
UIDevice.current.userInterfaceIdiom
}
var themeIsiPadPortrait: Bool {
#if targetEnvironment(macCatalyst)
false
@ -39,7 +39,7 @@ extension View {
return device.userInterfaceIdiom == .pad && device.orientation.isPortrait
#endif
}
var themeIsiPadMultitasking: Bool {
#if targetEnvironment(macCatalyst)
false
@ -128,15 +128,15 @@ extension View {
fileprivate var themePrimaryBackgroundColor: Color {
Color(Asset.Assets.primaryColor.color)
}
fileprivate var themeSecondaryColor: Color {
.secondary
}
fileprivate var themeLightTextColor: Color {
Color(Asset.Assets.lightTextColor.color)
}
fileprivate var themeErrorColor: Color {
.red
}
@ -160,11 +160,11 @@ extension View {
var themeAssetsLogoImage: String {
"Logo"
}
var themeCheckmarkImage: String {
"checkmark"
}
var themeShareImage: String {
"square.and.arrow.up"
}
@ -172,11 +172,11 @@ extension View {
var themeCopyImage: String {
"doc.on.doc"
}
var themeCloseImage: String {
"xmark"
}
var themeConceilImage: String {
"eye.slash"
}
@ -186,11 +186,11 @@ extension View {
}
// MARK: Organizer
func themeAssetsProviderImage(_ providerName: ProviderName) -> String {
"providers/\(providerName)"
}
func themeAssetsCountryImage(_ countryCode: String) -> String {
"flags/\(countryCode.lowercased())"
}
@ -198,15 +198,15 @@ extension View {
var themeProviderImage: String {
"externaldrive.connected.to.line.below"
}
var themeHostFilesImage: String {
"folder"
}
var themeHostTextImage: String {
"text.justify"
}
var themeSettingsImage: String {
"gearshape"
}
@ -222,11 +222,11 @@ extension View {
var themeWriteReviewImage: String {
"star"
}
var themeAddMenuImage: String {
"plus"
}
var themeProfileActiveImage: String {
"checkmark.circle"
}
@ -257,19 +257,19 @@ extension View {
"highlighter"
// "character.cursor.ibeam"
}
var themeDuplicateImage: String {
"doc.on.doc"
}
var themeUninstallImage: String {
"arrow.uturn.down"
}
var themeDeleteImage: String {
"trash"
}
var themeVPNProtocolImage: String {
"bolt"
// "waveform.path.ecg"
@ -277,36 +277,36 @@ extension View {
// "pc"
// "captions.bubble.fill"
}
var themeEndpointImage: String {
"link"
}
var themeAccountImage: String {
"person"
}
var themeProviderLocationImage: String {
"location"
}
var themeProviderPresetImage: String {
"slider.horizontal.3"
}
var themeNetworkSettingsImage: String {
// "network"
"globe"
}
var themeOnDemandImage: String {
"wifi"
}
var themeDiagnosticsImage: String {
"bandage.fill"
}
var themeFAQImage: String {
"questionmark.diamond"
}
@ -345,11 +345,11 @@ extension View {
func themeSecondaryTextStyle() -> some View {
foregroundColor(themeSecondaryColor)
}
func themeLightTextStyle() -> some View {
foregroundColor(themeLightTextColor)
}
@available(iOS 15, *)
func themePrimaryTintStyle() -> some View {
tint(themePrimaryBackgroundColor)
@ -363,7 +363,7 @@ extension View {
lineLimit(1)
.truncationMode(.middle)
}
func themeRawTextStyle() -> some View {
disableAutocorrection(true)
.autocapitalization(.none)
@ -381,7 +381,7 @@ extension View {
}
// MARK: Animations
extension View {
func themeAnimation<V: Equatable>(on value: V) -> some View {
animation(.default, value: value)
@ -416,7 +416,7 @@ extension View {
}
}
}
func themeSaveButtonLabel() -> some View {
// themeCheckmarkImage.asSystemImage
Text(L10n.Global.Strings.save)
@ -451,7 +451,7 @@ extension View {
.foregroundColor(themeSecondaryColor)
}
}
@ViewBuilder
func themeErrorMessage(_ message: String?) -> some View {
if let message = message {
@ -480,13 +480,13 @@ extension View {
.keyboardType(.asciiCapable)
.themeRawTextStyle()
}
func themeValidIPAddress(_ ipAddress: String?) -> some View {
themeValidating(ipAddress, validator: Validators.ipAddress)
.keyboardType(.numbersAndPunctuation)
.themeRawTextStyle()
}
func themeValidSocketPort() -> some View {
keyboardType(.numberPad)
}

View File

@ -30,20 +30,20 @@ import PassepartoutLibrary
@MainActor
class AppContext {
let logManager: LogManager
let productManager: ProductManager
private let reviewer: Reviewer
private var cancellables: Set<AnyCancellable> = []
init(coreContext: CoreContext) {
logManager = LogManager(logFile: Constants.Log.App.url)
logManager.logLevel = Constants.Log.level
logManager.logFormat = Constants.Log.App.format
logManager.configureLogging()
pp_log.info("Logging to: \(logManager.logFile!)")
productManager = ProductManager(
appType: Constants.InApp.appType,
buildProducts: Constants.InApp.buildProducts
@ -51,12 +51,12 @@ class AppContext {
reviewer = Reviewer()
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
// post
configureObjects(coreContext: coreContext)
}
private func configureObjects(coreContext: CoreContext) {
coreContext.vpnManager.isOnDemandRulesSupported = {
self.isEligibleForOnDemandRules()
@ -81,7 +81,7 @@ class AppContext {
}
}.store(in: &cancellables)
}
// eligibility: ignore network settings if ineligible
private func isEligibleForNetworkSettings() -> Bool {
guard productManager.isEligible(forFeature: .networkSettings) else {
@ -90,7 +90,7 @@ class AppContext {
}
return true
}
// eligibility: reset on-demand rules if no trusted networks
private func isEligibleForOnDemandRules() -> Bool {
guard productManager.isEligible(forFeature: .trustedNetworks) else {

View File

@ -38,7 +38,7 @@ extension ProviderMetadata: Identifiable, Comparable, Hashable {
public static func <(lhs: Self, rhs: Self) -> Bool {
lhs.fullName.lowercased() < rhs.fullName.lowercased()
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
@ -68,7 +68,7 @@ extension ProviderServer: Comparable {
public static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
// "Default" comes first (nil localizedName)
public static func <(lhs: Self, rhs: Self) -> Bool {
guard lhs.localizedName != rhs.localizedName else {
@ -113,7 +113,7 @@ extension ProviderMetadata {
}
return URL(string: string)
}
var referralURL: URL? {
guard let string = Constants.URLs.referrals[name] else {
return nil

View File

@ -28,7 +28,7 @@ import PassepartoutLibrary
protocol ProviderProfileAvailability {
var profile: Profile { get }
var providerManager: ProviderManager { get }
}

View File

@ -31,12 +31,12 @@ extension VPNProtocolType {
let protos: [Self] = [.openVPN, .wireGuard]
return protos.map(\.fileExtension)
}()
var fileExtension: String {
switch self {
case .openVPN:
return "ovpn"
case .wireGuard:
return "conf"
}

View File

@ -27,11 +27,11 @@ import Foundation
struct BuildProducts {
private let productsAtBuild: (Int) -> [LocalProduct]
init(productsAtBuild: @escaping (Int) -> [LocalProduct]) {
self.productsAtBuild = productsAtBuild
}
func products(atBuild build: Int) -> [LocalProduct] {
productsAtBuild(build)
}

View File

@ -33,13 +33,13 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
private static let bundle = "com.algoritmico.\(bundleSubdomain).Passepartout"
private static let donationsBundle = "\(bundle).donations"
private static let featuresBundle = "\(bundle).features"
static let providersBundle = "\(bundle).providers"
// MARK: Donations
static let tinyDonation = LocalProduct(donationDescription: "Tiny")
static let smallDonation = LocalProduct(donationDescription: "Small")
@ -51,7 +51,7 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
static let hugeDonation = LocalProduct(donationDescription: "Huge")
static let maxiDonation = LocalProduct(donationDescription: "Maxi")
static let allDonations: [LocalProduct] = [
.tinyDonation,
.smallDonation,
@ -66,9 +66,9 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
}
// MARK: Features
static let allProviders = LocalProduct(featureId: "all_providers")
static let networkSettings = LocalProduct(featureId: "network_settings")
static let trustedNetworks = LocalProduct(featureId: "trusted_networks")
@ -76,7 +76,7 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
static let siriShortcuts = LocalProduct(featureId: "siri")
static let fullVersion_iOS = LocalProduct(featureId: "full_version")
static let fullVersion_macOS = LocalProduct(featureId: "full_mac_version")
static let fullVersion = LocalProduct(featureId: "full_multi_version")
@ -100,7 +100,7 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
static var all: [LocalProduct] {
allDonations + allFeatures// + allProviders
}
var isDonation: Bool {
rawValue.hasPrefix(LocalProduct.donationsBundle)
}
@ -112,11 +112,11 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
var isProvider: Bool {
rawValue.hasPrefix(LocalProduct.providersBundle)
}
// MARK: RawRepresentable
let rawValue: String
init?(rawValue: String) {
self.rawValue = rawValue
}
@ -140,22 +140,22 @@ private extension ProviderName {
switch self {
case .mullvad:
return "Mullvad"
case .nordvpn:
return "NordVPN"
case .pia:
return "PIA"
case .protonvpn:
return "ProtonVPN"
case .tunnelbear:
return "TunnelBear"
case .vyprvpn:
return "VyprVPN"
case .windscribe:
return "Windscribe"

View File

@ -31,7 +31,7 @@ import Combine
enum ProductError: Error {
case uneligible
case beta
}
@ -40,30 +40,30 @@ class ProductManager: NSObject, ObservableObject {
case freemium = 0
case beta = 1
case fullVersion = 2
}
let appType: AppType
let buildProducts: BuildProducts
let didRefundProducts = PassthroughSubject<Void, Never>()
@Published private(set) var isRefreshingProducts = false
@Published private(set) var products: [SKProduct]
//
private let inApp: InApp<LocalProduct>
private var purchasedAppBuild: Int?
private var purchasedFeatures: Set<LocalProduct>
private var purchaseDates: [LocalProduct: Date]
private var cancelledPurchases: Set<LocalProduct>? {
willSet {
guard cancelledPurchases != nil else {
@ -76,20 +76,20 @@ class ProductManager: NSObject, ObservableObject {
detectRefunds(newCancelledPurchases)
}
}
private var refreshRequest: SKReceiptRefreshRequest?
init(appType: AppType, buildProducts: BuildProducts) {
self.appType = appType
self.buildProducts = buildProducts
products = []
inApp = InApp()
purchasedAppBuild = nil
purchasedFeatures = []
purchaseDates = [:]
cancelledPurchases = nil
super.init()
reloadReceipt()
@ -97,15 +97,15 @@ class ProductManager: NSObject, ObservableObject {
refreshProducts()
}
deinit {
SKPaymentQueue.default().remove(self)
}
func canMakePayments() -> Bool {
SKPaymentQueue.canMakePayments()
}
func refreshProducts() {
let ids = LocalProduct.all
guard !ids.isEmpty else {
@ -130,7 +130,7 @@ class ProductManager: NSObject, ObservableObject {
func product(withIdentifier identifier: LocalProduct) -> SKProduct? {
inApp.product(withIdentifier: identifier)
}
func featureProducts(including: [LocalProduct]) -> [SKProduct] {
inApp.products.filter {
guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
@ -145,7 +145,7 @@ class ProductManager: NSObject, ObservableObject {
return true
}
}
func featureProducts(excluding: [LocalProduct]) -> [SKProduct] {
inApp.products.filter {
guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
@ -160,7 +160,7 @@ class ProductManager: NSObject, ObservableObject {
return true
}
}
func purchase(_ product: SKProduct, completionHandler: @escaping (Result<InAppPurchaseResult, Error>) -> Void) {
inApp.purchase(product: product) { result in
if case .success = result {
@ -171,7 +171,7 @@ class ProductManager: NSObject, ObservableObject {
}
}
}
func restorePurchases(completionHandler: @escaping (Error?) -> Void) {
inApp.restorePurchases { (finished, _, error) in
guard finished else {
@ -184,7 +184,7 @@ class ProductManager: NSObject, ObservableObject {
}
// MARK: In-app eligibility
private func isCurrentPlatformVersion() -> Bool {
purchasedFeatures.contains(isMac ? .fullVersion_macOS : .fullVersion_iOS)
}
@ -222,7 +222,7 @@ class ProductManager: NSObject, ObservableObject {
func isEligibleForFeedback() -> Bool {
appType == .beta || !purchasedFeatures.isEmpty
}
func hasPurchased(_ product: LocalProduct) -> Bool {
purchasedFeatures.contains(product)
}
@ -257,7 +257,7 @@ class ProductManager: NSObject, ObservableObject {
}
if let iapReceipts = receipt.inAppPurchaseReceipts {
purchaseDates.removeAll()
pp_log.debug("In-app receipts:")
iapReceipts.forEach {
guard let pid = $0.productIdentifier, let product = LocalProduct(rawValue: pid) else {
@ -296,7 +296,7 @@ extension ProductManager {
let isEligibleForFullVersion = isFullVersion()
let hasCancelledFullVersion: Bool
let hasCancelledTrustedNetworks: Bool
if isMac {
hasCancelledFullVersion = !isEligibleForFullVersion && (
refunds.contains(.fullVersion) || refunds.contains(.fullVersion_macOS)
@ -308,7 +308,7 @@ extension ProductManager {
)
hasCancelledTrustedNetworks = !isEligibleForFullVersion && refunds.contains(.trustedNetworks)
}
// review features and potentially revert them if they were used (Siri is handled in AppDelegate)
if hasCancelledFullVersion || hasCancelledTrustedNetworks {
didRefundProducts.send()

View File

@ -36,10 +36,10 @@ extension IntentDispatcher {
case activeAndConnected(UUID)
}
typealias VPNIntentActivity = IntentActivity<VPNManager>
static let enableVPN = VPNIntentActivity(name: Constants.Activities.enableVPN) { activity, vpnManager in
static let enableVPN = VPNIntentActivity(name: Constants.Activities.enableVPN) { _, vpnManager in
pp_log.info("Enabling VPN...")
Task {
@ -51,7 +51,7 @@ extension IntentDispatcher {
}
}
static let disableVPN = VPNIntentActivity(name: Constants.Activities.disableVPN) { activity, vpnManager in
static let disableVPN = VPNIntentActivity(name: Constants.Activities.disableVPN) { _, vpnManager in
pp_log.info("Disabling VPN...")
Task {
@ -81,7 +81,7 @@ extension IntentDispatcher {
}
}
}
static let moveToLocation = VPNIntentActivity(name: Constants.Activities.moveToLocation) { activity, vpnManager in
pp_log.info("Moving to VPN location...")
@ -110,27 +110,27 @@ extension IntentDispatcher {
}
}
}
static let trustCellularNetwork = VPNIntentActivity(name: Constants.Activities.trustCellularNetwork) { activity, vpnManager in
static let trustCellularNetwork = VPNIntentActivity(name: Constants.Activities.trustCellularNetwork) { _, vpnManager in
pp_log.info("Trusting mobile network...")
handleCellularNetwork(true, vpnManager)
}
static let trustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.trustCurrentNetwork) { activity, vpnManager in
static let trustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.trustCurrentNetwork) { _, vpnManager in
pp_log.info("Trusting current Wi-Fi...")
handleCurrentNetwork(true, vpnManager)
}
static let untrustCellularNetwork = VPNIntentActivity(name: Constants.Activities.untrustCellularNetwork) { activity, vpnManager in
static let untrustCellularNetwork = VPNIntentActivity(name: Constants.Activities.untrustCellularNetwork) { _, vpnManager in
pp_log.info("Untrusting mobile network...")
handleCellularNetwork(false, vpnManager)
}
static let untrustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.untrustCurrentNetwork) { activity, vpnManager in
static let untrustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.untrustCurrentNetwork) { _, vpnManager in
pp_log.info("Untrusting current Wi-Fi...")
handleCurrentNetwork(false, vpnManager)
}
private static func handleCellularNetwork(_ trust: Bool, _ vpnManager: VPNManager) {
Task {
do {

View File

@ -30,19 +30,19 @@ import PassepartoutLibrary
class IntentDispatcher {
private struct Groups {
static let vpn = "VPN"
static let trust = "Trust"
}
// MARK: Intents
static func intentConnect(header: Profile.Header) -> ConnectVPNIntent {
let intent = ConnectVPNIntent()
intent.profileId = header.id.uuidString
intent.profileName = header.name
return intent
}
static func intentMoveTo(header: Profile.Header, providerFullName: String, server: ProviderServer) -> MoveToLocationIntent {
let intent = MoveToLocationIntent()
intent.profileId = header.id.uuidString
@ -51,33 +51,33 @@ class IntentDispatcher {
intent.serverName = server.localizedLongDescription(withCategory: false)
return intent
}
static func intentEnable() -> EnableVPNIntent {
EnableVPNIntent()
}
static func intentDisable() -> DisableVPNIntent {
DisableVPNIntent()
}
static func intentTrustWiFi() -> TrustCurrentNetworkIntent {
TrustCurrentNetworkIntent()
}
static func intentUntrustWiFi() -> UntrustCurrentNetworkIntent {
UntrustCurrentNetworkIntent()
}
static func intentTrustCellular() -> TrustCellularNetworkIntent {
TrustCellularNetworkIntent()
}
static func intentUntrustCellular() -> UntrustCellularNetworkIntent {
UntrustCellularNetworkIntent()
}
// MARK: Donations
static func donateConnection(with profile: Profile, providerManager: ProviderManager) {
let genericIntent: INIntent
if let providerName = profile.header.providerName {
@ -93,24 +93,24 @@ class IntentDispatcher {
} else {
genericIntent = intentConnect(header: profile.header)
}
let interaction = INInteraction(intent: genericIntent, response: nil)
interaction.groupIdentifier = profile.id.uuidString
interaction.donateAndLog()
}
static func donateEnableVPN() {
let interaction = INInteraction(intent: intentEnable(), response: nil)
interaction.groupIdentifier = Groups.vpn
interaction.donateAndLog()
}
static func donateDisableVPN() {
let interaction = INInteraction(intent: intentDisable(), response: nil)
interaction.groupIdentifier = Groups.vpn
interaction.donateAndLog()
}
static func donateTrustCurrentNetwork() {
let interaction = INInteraction(intent: intentTrustWiFi(), response: nil)
interaction.groupIdentifier = Groups.trust
@ -122,19 +122,19 @@ class IntentDispatcher {
interaction.groupIdentifier = Groups.trust
interaction.donateAndLog()
}
static func donateTrustCellularNetwork() {
let interaction = INInteraction(intent: intentTrustCellular(), response: nil)
interaction.groupIdentifier = Groups.trust
interaction.donateAndLog()
}
static func donateUntrustCellularNetwork() {
let interaction = INInteraction(intent: intentUntrustCellular(), response: nil)
interaction.groupIdentifier = Groups.trust
interaction.donateAndLog()
}
static func forgetProfile(withHeader header: Profile.Header) {
INInteraction.delete(with: header.id.uuidString) { (error) in
if let error = error {

View File

@ -32,20 +32,20 @@ import PassepartoutLibrary
@MainActor
class IntentsManager: NSObject, ObservableObject {
@Published private(set) var isReloadingShortcuts = false
@Published private(set) var shortcuts: [UUID: Shortcut] = [:]
let shouldDismissIntentView = PassthroughSubject<Void, Never>()
private var continuation: CheckedContinuation<[INVoiceShortcut], Never>?
override init() {
super.init()
Task {
await reloadShortcuts()
}
}
func reloadShortcuts() async {
isReloadingShortcuts = true
do {
@ -81,10 +81,10 @@ extension IntentsManager: INUIEditVoiceShortcutViewControllerDelegate {
guard let vs = voiceShortcut else {
return
}
shortcuts[vs.identifier] = Shortcut(vs)
shouldDismissIntentView.send()
// XXX: iOS bug, vs.invocationPhrase here is still the old one before edit
//
// additionally, back from edit view controller does not trigger either onAppear or

View File

@ -27,11 +27,11 @@ import Foundation
class MacBundle {
static let shared = MacBundle()
private var bridge: MacBridge!
private lazy var bridgeDelegate = MacBundleDelegate(bundle: self)
@MainActor
func configure() {
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent(Constants.Plugins.macBridgeName) else {
@ -46,11 +46,11 @@ class MacBundle {
bridge = bridgeClass.init()
bridge.menu.delegate = bridgeDelegate
}
var utils: MacUtils {
bridge.utils
}
var menu: MacMenu {
bridge.menu
}

View File

@ -27,17 +27,17 @@ import Foundation
class MacBundleDelegate: MacMenuDelegate {
private weak var bundle: MacBundle?
@MainActor
var profileManager: LightProfileManager {
DefaultLightProfileManager()
}
@MainActor
var providerManager: LightProviderManager {
DefaultLightProviderManager()
}
@MainActor
var vpnManager: LightVPNManager {
DefaultLightVPNManager()
@ -46,7 +46,7 @@ class MacBundleDelegate: MacMenuDelegate {
var utils: LightUtils {
DefaultLightUtils()
}
init(bundle: MacBundle?) {
self.bundle = bundle
}

View File

@ -29,17 +29,17 @@ import Combine
class DefaultLightProfile: LightProfile {
let id: UUID
let name: String
let vpnProtocol: String
let isActive: Bool
let providerName: String?
let providerServer: LightProviderServer?
init(_ header: Profile.Header, vpnProtocol: String, isActive: Bool, providerServer: LightProviderServer?) {
id = header.id
name = header.name
@ -52,11 +52,11 @@ class DefaultLightProfile: LightProfile {
class DefaultLightProfileManager: LightProfileManager {
private let profileManager = ProfileManager.shared
private let providerManager = ProviderManager.shared
private var subscriptions: Set<AnyCancellable> = []
weak var delegate: LightProfileManagerDelegate?
init() {
@ -66,7 +66,7 @@ class DefaultLightProfileManager: LightProfileManager {
self.delegate?.didUpdateProfiles()
}.store(in: &subscriptions)
}
var hasProfiles: Bool {
profileManager.hasProfiles
}

View File

@ -29,9 +29,9 @@ import PassepartoutLibrary
class DefaultLightProviderCategory: LightProviderCategory {
let name: String
var locations: [LightProviderLocation]
init(_ category: ProviderCategory) {
name = category.name
locations = category.locations
@ -42,13 +42,13 @@ class DefaultLightProviderCategory: LightProviderCategory {
class DefaultLightProviderLocation: LightProviderLocation {
let description: String
let id: String
let countryCode: String
let servers: [LightProviderServer]
init(_ location: ProviderLocation) {
description = location.localizedCountry
id = location.id
@ -61,15 +61,15 @@ class DefaultLightProviderLocation: LightProviderLocation {
class DefaultLightProviderServer: LightProviderServer {
let description: String
let longDescription: String
let categoryName: String
let locationId: String
let serverId: String
init(_ server: ProviderServer) {
description = server.localizedShortDescriptionWithDefault
longDescription = server.localizedLongDescription(withCategory: false)
@ -81,11 +81,11 @@ class DefaultLightProviderServer: LightProviderServer {
class DefaultLightProviderManager: LightProviderManager {
private let providerManager = ProviderManager.shared
private var subscriptions: Set<AnyCancellable> = []
weak var delegate: LightProviderManagerDelegate?
init() {
providerManager.didUpdateProviders
.receive(on: DispatchQueue.main)
@ -93,7 +93,7 @@ class DefaultLightProviderManager: LightProviderManager {
self.delegate?.didUpdateProviders()
}.store(in: &subscriptions)
}
func categories(_ name: String, vpnProtocol: String) -> [LightProviderCategory] {
guard let vpnProtocolType = VPNProtocolType(rawValue: vpnProtocol) else {
fatalError("Unrecognized VPN protocol: \(vpnProtocol)")

View File

@ -28,18 +28,18 @@ import SwiftUI
class DefaultLightUtils: LightUtils {
private let app: UIApplication
init() {
app = .shared
}
@AppStorage(AppPreference.launchesOnLogin.key) var launchesOnLogin = false
func requestScene() {
guard app.connectedScenes.isEmpty else {
return
}
app.requestSceneSessionActivation(nil, userActivity: nil, options: nil) { error in
app.requestSceneSessionActivation(nil, userActivity: nil, options: nil) { _ in
//
}
}

View File

@ -29,17 +29,17 @@ import Combine
class DefaultLightVPNManager: LightVPNManager {
private let vpnManager = VPNManager.shared
private var subscriptions: Set<AnyCancellable> = []
var isEnabled: Bool {
vpnManager.currentState.isEnabled
}
var vpnStatus: LightVPNStatus {
vpnManager.currentState.vpnStatus.asLightVPNStatus
}
private var delegates: [String: LightVPNManagerDelegate] = [:]
init() {
@ -63,7 +63,7 @@ class DefaultLightVPNManager: LightVPNManager {
)
}.store(in: &subscriptions)
}
func connect(with profileId: UUID) {
Task {
try? await vpnManager.connect(with: profileId)
@ -75,7 +75,7 @@ class DefaultLightVPNManager: LightVPNManager {
try? await vpnManager.connect(with: profileId, toServer: serverId)
}
}
func disconnect() {
Task {
await vpnManager.disable()
@ -97,11 +97,11 @@ class DefaultLightVPNManager: LightVPNManager {
await vpnManager.reconnect()
}
}
func addDelegate(_ delegate: LightVPNManagerDelegate, withIdentifier identifier: String) {
delegates[identifier] = delegate
}
func removeDelegate(withIdentifier identifier: String) {
delegates.removeValue(forKey: identifier)
}
@ -120,13 +120,13 @@ private extension VPNStatus {
switch self {
case .connected:
return .connected
case .connecting:
return .connecting
case .disconnected:
return .disconnected
case .disconnecting:
return .disconnecting
}

View File

@ -33,9 +33,9 @@ struct AddingTextField<Field: View, ActionLabel: View>: View {
let textField: (@escaping () -> Void) -> Field
let addLabel: () -> ActionLabel
var commitLabel: (() -> ActionLabel)?
@State private var isAdding = false
var body: some View {
@ -51,14 +51,14 @@ struct AddingTextField<Field: View, ActionLabel: View>: View {
}
}
}
private func doAdd() {
withAnimation {
onAdd?()
isAdding = true
}
}
private func doCommit() {
withAnimation {
onCommit?()

View File

@ -25,7 +25,7 @@
import SwiftUI
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
func ??<T>(lhs: Binding<T?>, rhs: T) -> Binding<T> {
Binding {
lhs.wrappedValue ?? rhs
} set: {
@ -34,15 +34,15 @@ func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
}
extension Binding {
func toString() -> Binding<String> where Value == Optional<URL> {
func toString() -> Binding<String> where Value == URL? {
.init {
wrappedValue?.absoluteString ?? ""
} set: {
wrappedValue = URL(string: $0) ?? wrappedValue
}
}
func toString<T>() -> Binding<String> where Value == Optional<T>, T: FixedWidthInteger {
func toString<T>() -> Binding<String> where Value == T?, T: FixedWidthInteger {
.init {
guard let v = wrappedValue else {
return ""

View File

@ -29,17 +29,17 @@ struct CopySavingButton<T: Equatable, Label: View>: View {
@Binding var original: T
@Binding var copy: T
var mapping: (T) -> T
let label: () -> Label
var saveAnyway = false
var onSave: (() -> Void)?
@State private var isLoaded = false
var body: some View {
Button(action: saveToOriginal, label: label)
.disabled(!canSave)

View File

@ -27,9 +27,9 @@ import SwiftUI
struct DestructiveButton<Label: View>: View {
let action: () -> Void
let label: () -> Label
var body: some View {
if #available(iOS 15, *) {
Button(role: .destructive, action: action, label: label)

View File

@ -38,21 +38,21 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
onEditingChanged: (Bool) -> Void,
onCommit: () -> Void
)
@Binding var elements: [String]
var allowsDuplicates = true
var mapping: ([IdentifiableString]) -> [IdentifiableString] = { $0 }
var onAdd: ((Binding<String>) -> Void)?
let textField: (FieldCallback) -> Field
let addLabel: () -> ActionLabel
var commitLabel: (() -> ActionLabel)?
@State private var isLoaded = false
@State private var identifiableElements: [IdentifiableString] = []
@ -60,7 +60,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
@State private var editedTextStrings: [UUID: String] = [:]
private let addedUUID = UUID()
private var addedText: Binding<String> {
.init {
editedTextStrings[addedUUID] ?? ""
@ -68,7 +68,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
editedTextStrings[addedUUID] = $0
}
}
var body: some View {
debugChanges()
return Group {
@ -87,7 +87,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
}
}.onChange(of: elements, perform: remapElements)
}
private func existingRow(_ element: IdentifiableString) -> some View {
let editedText = binding(toEditedElement: element)
@ -100,7 +100,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
replaceElement(at: element.id, with: editedText)
}))
}
private var newRow: some View {
AddingTextField(
onAdd: {
@ -144,7 +144,7 @@ extension EditableTextList {
identifiableElements = newIdentifiableElements
}
}
private func addElement() {
guard allowsDuplicates || !identifiableElements.contains(where: {
$0.string == addedText.wrappedValue
@ -155,7 +155,7 @@ extension EditableTextList {
identifiableElements.append(.init(string: addedText.wrappedValue))
commit()
}
private func binding(toEditedElement element: IdentifiableString) -> Binding<String> {
// print(">>> <-> \(element)")
.init {
@ -184,14 +184,14 @@ extension EditableTextList {
}
commit()
}
private func onDelete(offsets: IndexSet) {
var mapped = mapping(identifiableElements)
mapped.remove(atOffsets: offsets)
identifiableElements = mapped
commit()
}
private func onMove(indexSet: IndexSet, to: Int) {
var mapped = mapping(identifiableElements)
mapped.move(fromOffsets: indexSet, toOffset: to)

View File

@ -27,23 +27,23 @@ import SwiftUI
struct GenericCreditsView: View {
typealias License = (String, String, URL)
typealias Notice = (String, String)
var licensesHeader: String? = "Licenses"
var noticesHeader: String? = "Notices"
var translationsHeader: String? = "Translations"
let licenses: [License]
let notices: [Notice]
let translations: [String: String]
@State private var contentForLicense: [String: String] = [:]
var body: some View {
List {
if !licenses.isEmpty {
@ -57,27 +57,27 @@ struct GenericCreditsView: View {
}
}
}
private var sortedLicenses: [License] {
licenses.sorted {
$0.0.lowercased() < $1.0.lowercased()
}
}
private var sortedNotices: [Notice] {
notices.sorted {
$0.0.lowercased() < $1.0.lowercased()
}
}
private var sortedLanguages: [String] {
translations.keys.sorted {
$0.localizedAsCountryCode < $1.localizedAsCountryCode
}
}
private var licensesSection: some View {
Section (
Section(
header: licensesHeader.map(Text.init)
) {
ForEach(sortedLicenses, id: \.0) { license in
@ -98,7 +98,7 @@ struct GenericCreditsView: View {
}
private var noticesSection: some View {
Section (
Section(
header: noticesHeader.map(Text.init)
) {
ForEach(sortedNotices, id: \.0) { notice in
@ -108,7 +108,7 @@ struct GenericCreditsView: View {
}
private var translationsSection: some View {
Section (
Section(
header: translationsHeader.map(Text.init)
) {
ForEach(sortedLanguages, id: \.self) { code in
@ -123,7 +123,7 @@ struct GenericCreditsView: View {
}
}
}
private func noticeView(_ content: (String, String)) -> some View {
VStack {
Text(content.1)
@ -137,9 +137,9 @@ struct GenericCreditsView: View {
extension GenericCreditsView {
struct LicenseView: View {
let url: URL
@Binding var content: String?
var body: some View {
ZStack {
content.map { unwrapped in
@ -154,7 +154,7 @@ extension GenericCreditsView {
}
}.onAppear(perform: loadURL)
}
private func loadURL() {
guard content == nil else {
return

View File

@ -27,13 +27,13 @@ import SwiftUI
struct GenericVersionView: View {
let logoName: String
let appName: String
let versionString: String
let extraString: String?
var body: some View {
ScrollView {
Image(logoName)

View File

@ -27,7 +27,7 @@ import SwiftUI
public struct IntentActivity<UserObject> {
public let name: String
public let handler: (NSUserActivity, UserObject) -> Void
}

View File

@ -29,9 +29,9 @@ import IntentsUI
struct IntentAddView: UIViewControllerRepresentable {
let shortcut: INShortcut
let delegate: INUIAddVoiceShortcutViewControllerDelegate?
func makeUIViewController(context: UIViewControllerRepresentableContext<IntentAddView>) -> INUIAddVoiceShortcutViewController {
let vc = INUIAddVoiceShortcutViewController(shortcut: shortcut)
vc.delegate = delegate

View File

@ -29,9 +29,9 @@ import IntentsUI
struct IntentEditView: UIViewControllerRepresentable {
let shortcut: Shortcut
let delegate: INUIEditVoiceShortcutViewControllerDelegate?
func makeUIViewController(context: UIViewControllerRepresentableContext<IntentEditView>) -> INUIEditVoiceShortcutViewController {
let vc = INUIEditVoiceShortcutViewController(voiceShortcut: shortcut.native)
vc.delegate = delegate

View File

@ -27,7 +27,7 @@ import SwiftUI
struct LongContentView: View {
@Binding var content: String
var body: some View {
TextEditor(text: $content)
.font(.system(.body, design: .monospaced))
@ -39,13 +39,13 @@ struct LongContentView: View {
struct LongContentLink<Preview: View>: View {
private let title: String
@Binding private var content: String
private let preview: String?
private let previewLabel: ((String) -> Preview)?
init(
_ title: String,
content: Binding<String>,
@ -57,7 +57,7 @@ struct LongContentLink<Preview: View>: View {
self.preview = preview
self.previewLabel = previewLabel
}
var body: some View {
NavigationLink {
LongContentView(content: $content)

View File

@ -29,32 +29,32 @@ import MessageUI
struct MailComposerView: UIViewControllerRepresentable {
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
@Binding private var isPresented: Bool
init(_ view: MailComposerView) {
_isPresented = view._isPresented
}
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
isPresented = false
}
}
typealias Attachment = (data: Data, mimeType: String, fileName: String)
static func canSendMail() -> Bool {
MFMailComposeViewController.canSendMail()
}
@Binding var isPresented: Bool
let toRecipients: [String]
let subject: String
let messageBody: String
var attachments: [Attachment]?
func makeUIViewController(context: UIViewControllerRepresentableContext<MailComposerView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.setToRecipients(toRecipients)
@ -69,7 +69,7 @@ struct MailComposerView: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: UIViewControllerRepresentableContext<MailComposerView>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}

View File

@ -27,15 +27,15 @@ import SwiftUI
struct RevealingSecureField<ImageContent: View>: View {
let title: String
@Binding private var text: String
private let conceilImage: () -> ImageContent
private let revealImage: () -> ImageContent
@State private var isRevealed = false
init(
_ title: String,
text: Binding<String>,
@ -47,7 +47,7 @@ struct RevealingSecureField<ImageContent: View>: View {
self.conceilImage = conceilImage
self.revealImage = revealImage
}
var body: some View {
HStack {
if isRevealed {

View File

@ -29,14 +29,14 @@ import StoreKit
public class Reviewer: ObservableObject {
private struct Keys {
static let eventCount = "Reviewer.EventCount"
static let lastVersion = "Reviewer.LastVersion"
}
private let defaults: UserDefaults
public var eventCountBeforeRating: Int = .max
public init() {
defaults = .standard
}
@ -66,7 +66,7 @@ public class Reviewer: ObservableObject {
count += eventCount
defaults.set(count, forKey: Keys.eventCount)
print("Reviewer: Event reported for version \(currentVersion) (count: \(count), prompt: \(eventCountBeforeRating))")
guard count >= eventCountBeforeRating else {
return false
}
@ -74,11 +74,11 @@ public class Reviewer: ObservableObject {
defaults.removeObject(forKey: Keys.eventCount)
defaults.set(currentVersion, forKey: Keys.lastVersion)
requestReview()
return true
}
// may or may not appear
private func requestReview() {
guard let scene = UIApplication.shared.windows.first(where: { $0.windowScene != nil })?.windowScene else {

View File

@ -28,11 +28,11 @@ import Intents
struct Shortcut: Identifiable, Hashable, Comparable {
let native: INVoiceShortcut
init(_ native: INVoiceShortcut) {
self.native = native
}
var id: UUID {
native.identifier
}
@ -40,7 +40,7 @@ struct Shortcut: Identifiable, Hashable, Comparable {
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.phrase == rhs.phrase
}
static func <(lhs: Self, rhs: Self) -> Bool {
lhs.phrase < rhs.phrase
}

View File

@ -27,15 +27,15 @@ import SwiftUI
struct StyledPicker<T: Hashable, Label: View, Style: ListStyle>: View {
let title: String
@Binding var selection: T
let values: [T]
let pickerLabel: (T) -> Label
let selectionLabel: (T) -> Label
let listStyle: () -> Style
@State private var isPresented = false
@ -49,7 +49,7 @@ struct StyledPicker<T: Hashable, Label: View, Style: ListStyle>: View {
}
}
}
private func pickerView() -> some View {
List {
Section {

View File

@ -28,13 +28,13 @@ import Foundation
struct Validators {
enum ValidationError: Error {
case notSet
case empty
case ipAddress
case domainName
case url
}
@ -71,7 +71,7 @@ struct Validators {
throw ValidationError.domainName
}
}
static func url(_ string: String) throws {
guard let _ = URL(string: string) else {
throw ValidationError.url

View File

@ -31,7 +31,7 @@ struct AboutView: View {
private let versionString = Constants.Global.appVersionString
private let redditURL = Constants.URLs.subreddit
private let shareMessage = L10n.Global.Messages.share
private let readmeURL = Constants.URLs.readme
@ -55,7 +55,7 @@ struct AboutView: View {
}.themeSecondaryView()
.navigationTitle(L10n.About.title)
}
private var infoSection: some View {
Section {
NavigationLink {
@ -69,7 +69,7 @@ struct AboutView: View {
}
}
}
private var supportSection: some View {
Section {
Button(L10n.About.Items.JoinCommunity.caption) {

View File

@ -28,19 +28,19 @@ import PassepartoutLibrary
struct AccountView: View {
@ObservedObject private var providerManager: ProviderManager
private let providerName: ProviderName?
private let vpnProtocol: VPNProtocolType
@Binding private var account: Profile.Account
private let saveAnyway: Bool
private let onSave: (() -> Void)?
@State private var liveAccount = Profile.Account()
init(
providerName: ProviderName?,
vpnProtocol: VPNProtocolType,
@ -55,7 +55,7 @@ struct AccountView: View {
self.saveAnyway = saveAnyway
self.onSave = onSave
}
var body: some View {
List {
Section {
@ -126,7 +126,7 @@ struct AccountView: View {
)
}
}
private func openGuidanceURL(_ url: URL) {
URL.openURL(url)
}
@ -155,10 +155,10 @@ private extension Profile.Account.AuthenticationMethod {
switch self {
case .persistent:
return L10n.Account.Items.AuthenticationMethod.persistent
case .interactive:
return L10n.Account.Items.AuthenticationMethod.interactive
case .totp:
return Unlocalized.Other.totp
}

View File

@ -31,15 +31,15 @@ import TunnelKitWireGuard
extension AddHostView {
struct NameView: View {
@ObservedObject private var profileManager: ProfileManager
private let url: URL
private let deletingURLOnSuccess: Bool
private let bindings: AddProfileView.Bindings
@State private var viewModel = ViewModel()
@State private var isEnteringCredentials = false
private var isComplete: Bool {
@ -107,7 +107,7 @@ extension AddHostView {
completeSection
}
}
private var encryptionSection: some View {
Section {
SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) {
@ -132,7 +132,7 @@ extension AddHostView {
themeErrorMessage(viewModel.errorMessage)
}
}
private var hiddenAccountLink: some View {
NavigationLink("", isActive: $isEnteringCredentials) {
AddProfileView.AccountWrapperView(
@ -153,11 +153,11 @@ extension AddHostView {
private func requestResourcePermissions() {
_ = url.startAccessingSecurityScopedResource()
}
private func dropResourcePermissions() {
url.stopAccessingSecurityScopedResource()
}
private func alertOverwriteExistingProfile() -> Alert {
Alert(
title: Text(L10n.AddProfile.Shared.title),

View File

@ -31,19 +31,19 @@ import TunnelKitWireGuard
extension AddHostView {
struct ViewModel: Equatable {
private var isNamePreset = false
var profileName = ""
private(set) var requiresPassphrase = false
var encryptionPassphrase = ""
var processedProfile: Profile = .placeholder
private(set) var errorMessage: String?
var isAskingOverwrite = false
mutating func presetName(withURL url: URL) {
guard !isNamePreset else {
return
@ -96,7 +96,7 @@ extension AddHostView {
setMessage(forParsingError: error)
}
}
@MainActor
mutating func addProcessedProfile(to profileManager: ProfileManager) -> Bool {
guard !processedProfile.isPlaceholder else {
@ -107,7 +107,7 @@ extension AddHostView {
profileManager.saveProfile(processedProfile, isActive: nil)
return true
}
private mutating func setMessage(forParsingError error: Error) {
errorMessage = error.localizedVPNParsingDescription
}

View File

@ -31,7 +31,7 @@ struct AddProfileMenu: View {
case addProvider
case addHost(URL, Bool)
// XXX: alert ids
var id: Int {
switch self {
@ -41,16 +41,16 @@ struct AddProfileMenu: View {
}
}
}
@Binding private var modalType: ModalType?
@Binding private var isHostFileImporterPresented: Bool
init(modalType: Binding<ModalType?>, isHostFileImporterPresented: Binding<Bool>) {
_modalType = modalType
_isHostFileImporterPresented = isHostFileImporterPresented
}
var body: some View {
Menu {
Button {
@ -74,7 +74,7 @@ struct AddProfileMenu: View {
themeAddMenuImage.asSystemImage
}.sheet(item: $modalType, content: presentedModal)
}
@ViewBuilder
private func presentedModal(_ modalType: ModalType) -> some View {
switch modalType {
@ -99,7 +99,7 @@ struct AddProfileMenu: View {
}.themeGlobal()
}
}
private var isModalPresented: Binding<Bool> {
.init {
modalType != nil

View File

@ -30,14 +30,14 @@ enum AddProfileView {
struct Bindings {
@Binding var isPresented: Bool
}
struct ProfileNameSection: View {
@Binding var profileName: String
let errorMessage: String?
let onCommit: () -> Void
var body: some View {
Section {
TextField(L10n.Global.Placeholders.profileName, text: $profileName, onCommit: onCommit)
@ -49,12 +49,12 @@ enum AddProfileView {
}
}
}
struct ExistingProfilesSection: View {
let headers: [Profile.Header]
@Binding var profileName: String
var body: some View {
Section {
ForEach(headers, content: existingProfileButton)
@ -62,7 +62,7 @@ enum AddProfileView {
Text(L10n.AddProfile.Shared.Views.Existing.header)
}
}
private func existingProfileButton(_ header: Profile.Header) -> some View {
Button(header.name) {
profileName = header.name
@ -74,11 +74,11 @@ enum AddProfileView {
@ObservedObject private var profileManager: ProfileManager
@Binding private var profile: Profile
private let bindings: AddProfileView.Bindings
@State private var account = Profile.Account()
init(
profile: Binding<Profile>,
bindings: AddProfileView.Bindings
@ -87,7 +87,7 @@ enum AddProfileView {
_profile = profile
self.bindings = bindings
}
var body: some View {
AccountView(
providerName: profile.header.providerName,

View File

@ -29,17 +29,17 @@ import PassepartoutLibrary
extension AddProviderView {
struct NameView: View {
@ObservedObject private var profileManager: ProfileManager
@Binding private var profile: Profile
private let providerMetadata: ProviderMetadata
private let bindings: AddProfileView.Bindings
@State private var viewModel = ViewModel()
@State private var isEnteringCredentials = false
init(
profile: Binding<Profile>,
providerMetadata: ProviderMetadata,
@ -80,7 +80,7 @@ extension AddProviderView {
}.alert(isPresented: $viewModel.isAskingOverwrite, content: alertOverwriteExistingProfile)
.navigationTitle(providerMetadata.fullName)
}
private var hiddenAccountLink: some View {
NavigationLink("", isActive: $isEnteringCredentials) {
AddProfileView.AccountWrapperView(

View File

@ -28,26 +28,26 @@ import PassepartoutLibrary
struct AddProviderView: View {
@ObservedObject private var providerManager: ProviderManager
@ObservedObject private var productManager: ProductManager
private let bindings: AddProfileView.Bindings
@StateObject private var viewModel = ViewModel()
init(bindings: AddProfileView.Bindings) {
providerManager = .shared
productManager = .shared
self.bindings = bindings
}
private var providers: [ProviderMetadata] {
providerManager.allProviders()
.filter {
$0.supportedVPNProtocols.contains(viewModel.selectedVPNProtocol)
}.sorted()
}
private var availableVPNProtocols: [VPNProtocolType] {
var protos: Set<VPNProtocolType> = []
providers.forEach {
@ -57,7 +57,7 @@ struct AddProviderView: View {
}
return protos.sorted()
}
var body: some View {
ZStack {
ForEach(providers, id: \.navigationId, content: hiddenProviderLink)
@ -82,7 +82,7 @@ struct AddProviderView: View {
}.navigationTitle(L10n.AddProfile.Shared.title)
.themeSecondaryView()
}
private var mainSection: some View {
Section {
let protos = availableVPNProtocols
@ -99,7 +99,7 @@ struct AddProviderView: View {
Text(L10n.AddProfile.Provider.Sections.Vpn.footer)
}
}
private var providersSection: some View {
Section {
ForEach(providers, content: providerRow)
@ -107,7 +107,7 @@ struct AddProviderView: View {
themeErrorMessage(viewModel.errorMessage)
}.disabled(viewModel.isFetchingAnyProvider)
}
private func providerRow(_ metadata: ProviderMetadata) -> some View {
Button {
presentOrPurchaseProvider(metadata)
@ -115,7 +115,7 @@ struct AddProviderView: View {
Label(metadata.fullName, image: themeAssetsProviderImage(metadata.name))
}.withTrailingProgress(when: viewModel.isFetchingProvider(metadata.name))
}
private func hiddenProviderLink(_ metadata: ProviderMetadata) -> some View {
NavigationLink("", tag: metadata, selection: $viewModel.selectedProvider) {
NameView(
@ -132,7 +132,7 @@ struct AddProviderView: View {
}.withTrailingProgress(when: viewModel.isUpdatingIndex)
.disabled(viewModel.isUpdatingIndex)
}
// eligibility: select or purchase provider
private func presentOrPurchaseProvider(_ metadata: ProviderMetadata) {
guard productManager.isEligible(forProvider: metadata.name) else {

View File

@ -30,7 +30,7 @@ extension AddProviderView {
class ViewModel: ObservableObject {
enum PendingOperation {
case index
case provider(ProviderName)
}
@ -54,19 +54,19 @@ extension AddProviderView {
}
return false
}
@Published var selectedVPNProtocol: VPNProtocolType = .openVPN
@Published var selectedProvider: ProviderMetadata?
@Published var pendingProfile: Profile = .placeholder
@Published private(set) var pendingOperation: PendingOperation?
@Published var isPaywallPresented = false
@Published private(set) var errorMessage: String?
func selectProvider(_ metadata: ProviderMetadata, _ providerManager: ProviderManager) {
errorMessage = nil
guard let server = providerManager.anyDefaultServer(
@ -109,7 +109,7 @@ extension AddProviderView {
pendingProfile = Profile(metadata, server: server)
selectedProvider = metadata
}
func updateIndex(_ providerManager: ProviderManager) {
errorMessage = nil
pendingOperation = .index
@ -136,11 +136,11 @@ extension AddProviderView.NameView {
private var isNamePreset = false
var profileName = ""
var isAskingOverwrite = false
private(set) var errorMessage: String?
mutating func presetName(withMetadata metadata: ProviderMetadata) {
guard !isNamePreset else {
return
@ -173,7 +173,7 @@ extension AddProviderView.NameView {
profileManager.saveProfile(finalProfile, isActive: nil)
return finalProfile
}
private mutating func setMessage(forError error: Error) {
errorMessage = error.localizedDescription
}

View File

@ -28,8 +28,8 @@ import SwiftUI
struct CreditsView: View {
var body: some View {
GenericCreditsView(
licensesHeader: nil,//L10n.Credits.Sections.Licenses.header,
noticesHeader: nil,//L10n.Credits.Sections.Notices.header,
licensesHeader: nil,// L10n.Credits.Sections.Licenses.header,
noticesHeader: nil,// L10n.Credits.Sections.Notices.header,
translationsHeader: L10n.Global.Strings.translations,
licenses: Unlocalized.Credits.licenses,
notices: Unlocalized.Credits.notices,

View File

@ -29,23 +29,23 @@ import PassepartoutLibrary
struct DebugLogView: View {
private let title: String
private let url: URL
private let timer: AnyPublisher<Date, Never>
@State private var logLines: [String] = []
@State private var isSharing = false
private let maxBytes = UInt64(Constants.Log.maxBytes)
private let appName = Constants.Global.appName
private let appVersion = Constants.Global.appVersionString
private let shareFilename = Unlocalized.Issues.Filenames.debugLog
init(title: String, url: URL, refreshInterval: TimeInterval?) {
self.title = title
self.url = url
@ -58,7 +58,7 @@ struct DebugLogView: View {
.eraseToAnyPublisher()
}
}
var body: some View {
ScrollViewReader { scrollProxy in
ScrollView(showsIndicators: true) {
@ -96,22 +96,22 @@ struct DebugLogView: View {
Text(logLines[$0])
.frame(maxWidth: .infinity, alignment: .leading)
}
}//.padding()
}// .padding()
// TODO: layout, a slight padding would be nice, but it glitches on first touch
}
private func refreshLog(scrollingToLatestWith scrollProxy: ScrollViewProxy?) {
logLines = url.trailingLines(bytes: maxBytes)
if let scrollProxy = scrollProxy {
scrollToLatestUpdate(scrollProxy)
}
}
private func refreshLog(_: Date) {
refreshLog(scrollingToLatestWith: nil)
}
}
extension DebugLogView {
private func shareDebugLog() {
guard !logLines.isEmpty else {
@ -120,7 +120,7 @@ extension DebugLogView {
}
isSharing = true
}
private func sharingActivityView() -> some View {
ActivityView(activityItems: sharingItems)
}

View File

@ -31,7 +31,7 @@ extension DiagnosticsView {
struct OpenVPNView: View {
enum AlertType: Int, Identifiable {
case emailNotConfigured
var id: Int {
return rawValue
}
@ -40,23 +40,23 @@ extension DiagnosticsView {
@ObservedObject private var providerManager: ProviderManager
@ObservedObject private var vpnManager: VPNManager
@ObservedObject private var currentVPNState: ObservableVPNState
@ObservedObject private var productManager: ProductManager
private let providerName: ProviderName?
private var isEligibleForFeedback: Bool {
productManager.isEligibleForFeedback()
}
@State private var isReportingIssue = false
@State private var alertType: AlertType?
private let vpnProtocol: VPNProtocolType = .openVPN
init(providerName: ProviderName?) {
providerManager = .shared
vpnManager = .shared
@ -103,7 +103,7 @@ extension DiagnosticsView {
}.disabled(cfg == nil)
}
}
private var debugLogSection: some View {
Section {
DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL)
@ -114,7 +114,7 @@ extension DiagnosticsView {
Text(L10n.Diagnostics.Sections.DebugLog.footer)
}
}
private var issueReporterSection: some View {
Section {
Button(L10n.Diagnostics.Items.ReportIssue.caption, action: presentReportIssue)

View File

@ -30,9 +30,9 @@ import TunnelKitWireGuard
extension DiagnosticsView {
struct WireGuardView: View {
@ObservedObject private var vpnManager: VPNManager
private let providerName: ProviderName?
init(providerName: ProviderName?) {
vpnManager = .shared
self.providerName = providerName

View File

@ -28,7 +28,7 @@ import PassepartoutLibrary
struct DiagnosticsView: View {
let vpnProtocol: VPNProtocolType
let providerName: ProviderName?
var body: some View {
@ -38,7 +38,7 @@ struct DiagnosticsView: View {
DiagnosticsView.OpenVPNView(
providerName: providerName
)
case .wireGuard:
DiagnosticsView.WireGuardView(
providerName: providerName
@ -51,9 +51,9 @@ struct DiagnosticsView: View {
extension DiagnosticsView {
struct DebugLogSection: View {
let appLogURL: URL?
let tunnelLogURL: URL?
private let refreshInterval = Constants.Log.refreshInterval
var body: some View {
@ -68,7 +68,7 @@ extension DiagnosticsView {
refreshInterval: nil
)
}
private var tunnelLink: some View {
navigationLink(
withTitle: Unlocalized.VPN.vpn,

View File

@ -30,9 +30,9 @@ import PassepartoutLibrary
struct DonateView: View {
enum AlertType: Identifiable {
case thankYou
case purchaseFailed(Error)
// XXX: alert ids
var id: Int {
switch self {
@ -42,19 +42,19 @@ struct DonateView: View {
}
}
}
@Environment(\.scenePhase) private var scenePhase
@ObservedObject private var productManager: ProductManager
@State private var alertType: AlertType?
@State private var pendingDonationIdentifier: String?
init() {
productManager = .shared
}
var body: some View {
List {
productsSection
@ -90,7 +90,7 @@ struct DonateView: View {
)
}
}
private var productsSection: some View {
Section {
if !productManager.isRefreshingProducts {
@ -104,7 +104,7 @@ struct DonateView: View {
Text(L10n.Donate.Sections.OneTime.footer)
}
}
@ViewBuilder
private func productRow(_ product: SKProduct) -> some View {
HStack {
@ -129,7 +129,7 @@ extension DonateView {
pendingDonationIdentifier = product.productIdentifier
productManager.purchase(product, completionHandler: handlePurchaseResult)
}
private func handlePurchaseResult(_ result: Result<InAppPurchaseResult, Error>) {
switch result {
case .success(let value):
@ -138,7 +138,7 @@ extension DonateView {
} else {
// cancelled
}
case .failure(let error):
alertType = .purchaseFailed(error)
}

View File

@ -32,11 +32,11 @@ extension EndpointAdvancedView {
@Binding var builder: OpenVPN.ConfigurationBuilder
let isReadonly: Bool
let isServerPushed: Bool
private let fallbackConfiguration = OpenVPN.ConfigurationBuilder(withFallbacks: true).build()
var body: some View {
List {
let cfg = builder.build()
@ -104,7 +104,7 @@ extension EndpointAdvancedView.OpenVPNView {
Text(Unlocalized.Network.ipv4)
}
}
private var ipv6Section: some View {
Section {
if let settings = builder.ipv6 {
@ -156,7 +156,7 @@ extension EndpointAdvancedView.OpenVPNView {
}
}
}
private var communicationEditableSection: some View {
Section {
themeTextPicker(
@ -185,7 +185,7 @@ extension EndpointAdvancedView.OpenVPNView {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Communication.header)
}
}
private func compressionSection(configuration: OpenVPN.Configuration) -> some View {
configuration.compressionSettings.map { settings in
Section {
@ -202,7 +202,7 @@ extension EndpointAdvancedView.OpenVPNView {
}
}
}
private var compressionEditableSection: some View {
Section {
themeTextPicker(
@ -334,21 +334,21 @@ extension OpenVPN.Configuration {
}
return (compressionFraming, compressionAlgorithm)
}
var dnsSettings: (servers: [String], domains: [String])? {
guard !(dnsServers?.isEmpty ?? true) || !(searchDomains?.isEmpty ?? true) else {
return nil
}
return (dnsServers ?? [], searchDomains ?? [])
}
var proxySettings: (proxy: Proxy?, pac: URL?, bypass: [String])? {
guard httpsProxy != nil || httpProxy != nil || proxyAutoConfigurationURL != nil || !(proxyBypassDomains?.isEmpty ?? true) else {
return nil
}
return (httpsProxy ?? httpProxy, proxyAutoConfigurationURL, proxyBypassDomains ?? [])
}
var otherSettings: (keepAlive: TimeInterval?, reneg: TimeInterval?, randomizeEndpoint: Bool?, randomizeHostnames: Bool?)? {
guard keepAliveInterval != nil || renegotiatesAfter != nil || randomizeEndpoint != nil || randomizeHostnames != nil else {
return nil
@ -361,15 +361,15 @@ private extension EndpointAdvancedView.OpenVPNView {
var fallbackCipher: OpenVPN.Cipher {
fallbackConfiguration.cipher!
}
var fallbackDigest: OpenVPN.Digest {
fallbackConfiguration.digest!
}
var fallbackCompressionFraming: OpenVPN.CompressionFraming {
fallbackConfiguration.compressionFraming!
}
var fallbackCompressionAlgorithm: OpenVPN.CompressionAlgorithm {
fallbackConfiguration.compressionAlgorithm!
}

View File

@ -78,7 +78,7 @@ extension EndpointAdvancedView.WireGuardView {
}
}
}
private var mtuSection: some View {
builder.mtu.map { mtu in
Section {

View File

@ -36,21 +36,21 @@ extension EndpointView {
@ObservedObject private var currentProfile: ObservableProfile
@Binding private var builder: OpenVPN.ConfigurationBuilder
@Binding private var customEndpoint: Endpoint?
private var isConfigurationReadonly: Bool {
currentProfile.value.isProvider
}
@State private var isFirstAppearance = true
@State private var isAutomatic = false
@State private var selectedSocketType: SocketType = .udp
@State private var selectedPort: UInt16 = 0
// XXX: do not escape mutating 'self', use constant providerManager
init(currentProfile: ObservableProfile) {
let providerManager: ProviderManager = .shared
@ -104,7 +104,7 @@ extension EndpointView {
}
}
}
var body: some View {
ScrollViewReader { scrollProxy in
List {
@ -135,7 +135,7 @@ extension EndpointView.OpenVPNView {
Toggle(L10n.Global.Strings.automatic, isOn: $isAutomatic.themeAnimation())
}
}
private var filtersSection: some View {
Section {
themeTextPicker(
@ -162,7 +162,7 @@ extension EndpointView.OpenVPNView {
Text(L10n.Global.Strings.addresses)
}
}
private var advancedSection: some View {
Section {
let caption = L10n.Endpoint.Advanced.title
@ -175,7 +175,7 @@ extension EndpointView.OpenVPNView {
}
}
}
private func button(forEndpoint endpoint: Endpoint?) -> some View {
Button {
customEndpoint = endpoint
@ -265,7 +265,7 @@ extension EndpointView.OpenVPNView {
}.map(\.proto.port))
return Array(allPorts).sorted()
}
private var filteredRemotes: [Endpoint]? {
builder.remotes?.filter {
$0.proto.socketType == selectedSocketType && $0.proto.port == selectedPort

View File

@ -38,7 +38,7 @@ extension EndpointView {
@Binding private var builder: WireGuard.ConfigurationBuilder
// var customPeer: Binding<Endpoint?>? = nil
// XXX: do not escape mutating 'self', use constant providerManager
init(currentProfile: ObservableProfile, isReadonly: Bool) {
let providerManager: ProviderManager = .shared

View File

@ -28,11 +28,11 @@ import PassepartoutLibrary
struct EndpointView: View {
@ObservedObject private var currentProfile: ObservableProfile
init(currentProfile: ObservableProfile) {
self.currentProfile = currentProfile
}
var body: some View {
switch currentProfile.value.currentVPNProtocol {
case .openVPN:

View File

@ -34,7 +34,7 @@ struct NetworkSettingsView: View {
}
@State private var settings = Profile.NetworkSettings()
init(currentProfile: ObservableProfile) {
self.currentProfile = currentProfile
}
@ -64,7 +64,7 @@ struct NetworkSettingsView: View {
)
}
}
// EditButton()
// .disabled(!isAnythingManual)
@ -83,7 +83,7 @@ struct NetworkSettingsView: View {
// }
return false
}
private func mapNotEmpty(elements: [IdentifiableString]) -> [IdentifiableString] {
elements
.filter { !$0.string.isEmpty }
@ -110,7 +110,7 @@ extension NetworkSettingsView {
// MARK: DNS
extension NetworkSettingsView {
@ViewBuilder
private var dnsView: some View {
Section {
@ -146,7 +146,7 @@ extension NetworkSettingsView {
dnsManualDomains
}
}
private var dnsManualHTTPSRow: some View {
TextField(Unlocalized.Placeholders.dohURL, text: $settings.dns.dnsHTTPSURL.toString())
.themeValidURL(settings.dns.dnsHTTPSURL?.absoluteString)
@ -177,7 +177,7 @@ extension NetworkSettingsView {
}
}
}
private var dnsManualDomains: some View {
Section {
EditableTextList(
@ -203,7 +203,7 @@ extension NetworkSettingsView {
// MARK: Proxy
extension NetworkSettingsView {
@ViewBuilder
private var proxyView: some View {
Section {
@ -242,7 +242,7 @@ extension NetworkSettingsView {
proxyManualBypassDomains
}
}
private var proxyManualBypassDomains: some View {
Section {
EditableTextList(
@ -271,7 +271,7 @@ extension NetworkSettingsView {
private var mtuView: some View {
Section {
Toggle(L10n.Global.Strings.automatic, isOn: $settings.isAutomaticMTU.themeAnimation())
if !settings.isAutomaticMTU {
themeTextPicker(
L10n.Global.Strings.bytes,

View File

@ -29,9 +29,9 @@ import PassepartoutLibrary
extension OnDemandView {
struct SSIDList: View {
@Binding var withSSIDs: [String: Bool]
@StateObject private var reader = SSIDReader()
var body: some View {
EditableTextList(elements: allSSIDs, allowsDuplicates: false, mapping: mapElements) { text in
requestSSID(text)
@ -43,13 +43,13 @@ extension OnDemandView {
Text(L10n.Global.Strings.add)
}
}
private func mapElements(elements: [IdentifiableString]) -> [IdentifiableString] {
elements
.filter { !$0.string.isEmpty }
.sorted { $0.string.lowercased() < $1.string.lowercased() }
}
private func ssidRow(callback: EditableTextList.FieldCallback) -> some View {
Group {
if callback.isNewElement {
@ -61,7 +61,7 @@ extension OnDemandView {
}
}
}
private func ssidField(callback: EditableTextList.FieldCallback) -> some View {
TextField(
Unlocalized.Network.ssid,
@ -102,7 +102,7 @@ extension OnDemandView.SSIDList {
// print(">>> withSSIDs (allSSIDs): \(withSSIDs)")
}
}
private var onSSIDs: Binding<Set<String>> {
.init {
Set(withSSIDs.filter {
@ -128,7 +128,7 @@ extension OnDemandView.SSIDList {
// print(">>> withSSIDs (onSSIDs): \(withSSIDs)")
}
}
private func isSSIDOn(_ ssid: String) -> Binding<Bool> {
.init {
withSSIDs[ssid] ?? false

View File

@ -36,7 +36,7 @@ struct OnDemandView: View {
}
@State private var onDemand = Profile.OnDemand()
init(currentProfile: ObservableProfile) {
productManager = .shared
self.currentProfile = currentProfile
@ -72,7 +72,7 @@ extension OnDemandView {
Toggle(L10n.Global.Strings.enabled, isOn: $onDemand.isEnabled.themeAnimation())
}
}
@ViewBuilder
private var mainView: some View {
if Utils.hasCellularData() {
@ -118,7 +118,7 @@ extension OnDemandView {
IntentDispatcher.donateTrustCellularNetwork()
IntentDispatcher.donateUntrustCellularNetwork()
}
// eligibility: donate intents if eligible for Siri
private func donateNetworkIntents(_: [String: Bool]) {
guard isEligibleForSiri else {

View File

@ -29,11 +29,11 @@ import PassepartoutLibrary
extension OrganizerView {
struct ProfileRow: View {
private let profile: Profile
private let isActiveProfile: Bool
@Binding private var modalType: ModalType?
private var interactiveProfile: Binding<Profile?> {
.init {
if case .interactiveAccount(let profile) = modalType {
@ -48,13 +48,13 @@ extension OrganizerView {
}
}
}
init(profile: Profile, isActiveProfile: Bool, modalType: Binding<ModalType?>) {
self.profile = profile
self.isActiveProfile = isActiveProfile
_modalType = modalType
}
var body: some View {
debugChanges()
return HStack {
@ -62,7 +62,7 @@ extension OrganizerView {
Text(profile.header.name)
.font(.headline)
.themeLongTextStyle()
VPNStatusText(isActiveProfile: isActiveProfile)
.font(.subheadline)
.themeSecondaryTextStyle()

View File

@ -29,7 +29,7 @@ import PassepartoutLibrary
extension OrganizerView {
struct ProfilesList: View {
@ObservedObject private var profileManager: ProfileManager
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
@ -49,7 +49,7 @@ extension OrganizerView {
profileManager.currentProfileId = $0.id
}
}
private var mainView: some View {
List {
if profileManager.hasProfiles {
@ -69,7 +69,7 @@ extension OrganizerView {
}
}.themeAnimation(on: profileManager.headers)
}
private var profilesView: some View {
ForEach(sortedProfiles, content: profileRow(forProfile:))
.onDelete(perform: removeProfiles)
@ -91,7 +91,7 @@ extension OrganizerView {
ProfileContextMenu(header: profile.header)
}
}
private func profileLabel(forProfile profile: Profile) -> some View {
ProfileRow(
profile: profile,
@ -124,7 +124,7 @@ extension OrganizerView {
profileManager.removeProfiles(withIds: toDelete)
}
}
private func performMigrationsIfNeeded() {
Task { @MainActor in
UpgradeManager.shared.doMigrations(profileManager)
@ -140,13 +140,13 @@ extension OrganizerView {
@ObservedObject private var currentVPNState: ObservableVPNState
let header: Profile.Header
init(header: Profile.Header) {
profileManager = .shared
currentVPNState = .shared
self.header = header
}
var body: some View {
if #available(iOS 16, *), isActiveProfileNotDisconnected {
reconnectButton
@ -154,11 +154,11 @@ extension OrganizerView {
duplicateButton
deleteButton
}
private var isActiveProfileNotDisconnected: Bool {
profileManager.isActiveProfile(header.id) && currentVPNState.vpnStatus != .disconnected
}
private var reconnectButton: some View {
ProfileView.ReconnectButton()
}

View File

@ -31,13 +31,13 @@ extension OrganizerView {
@Environment(\.scenePhase) private var scenePhase
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var vpnManager: VPNManager
@Binding private var alertType: AlertType?
@Binding private var didHandleSubreddit: Bool
@State private var isFirstLaunch = true
init(alertType: Binding<AlertType?>, didHandleSubreddit: Binding<Bool>) {
@ -46,7 +46,7 @@ extension OrganizerView {
_alertType = alertType
_didHandleSubreddit = didHandleSubreddit
}
var body: some View {
// dummy text, EmptyView() does not trigger on*() handlers
@ -55,7 +55,7 @@ extension OrganizerView {
.onAppear(perform: onAppear)
.onChange(of: scenePhase, perform: onScenePhase)
}
private func onAppear() {
guard didHandleSubreddit else {
alertType = .subscribeReddit
@ -92,7 +92,7 @@ extension OrganizerView {
break
}
}
private func persist() {
profileManager.persist()
}

View File

@ -40,9 +40,9 @@ struct OrganizerView: View {
enum AlertType: Identifiable {
case subscribeReddit
case error(String, String)
// XXX: alert ids
var id: Int {
switch self {
@ -62,11 +62,11 @@ struct OrganizerView: View {
@State private var isHostFileImporterPresented = false
@AppStorage(AppPreference.didHandleSubreddit.key) private var didHandleSubreddit = false
private let hostFileTypes = Constants.URLs.filetypes
private let redditURL = Constants.URLs.subreddit
var body: some View {
debugChanges()
return ZStack {
@ -93,13 +93,13 @@ struct OrganizerView: View {
onCompletion: onHostFileImporterResult
).onOpenURL(perform: onOpenURL)
.themePrimaryView()
// VPN configuration error publisher (no need to observe VPNManager)
.onReceive(VPNManager.shared.configurationError) {
alertType = .error($0.profile.header.name, $0.error.localizedAppDescription)
}
}
private var hiddenSceneView: some View {
SceneView(
alertType: $alertType,
@ -128,7 +128,7 @@ extension OrganizerView {
)
}
}
private func onOpenURL(_ url: URL) {
addProfileModalType = .addHost(url, false)
}

View File

@ -37,7 +37,7 @@ extension PaywallView {
var id: Int {
switch self {
case .purchaseFailed: return 1
case .restoreFailed: return 2
}
}
@ -48,19 +48,19 @@ extension PaywallView {
case restoring
}
private typealias RowModel = (product: SKProduct, extra: String?)
@Environment(\.scenePhase) private var scenePhase
@ObservedObject private var productManager: ProductManager
@Binding private var isPresented: Bool
private let feature: LocalProduct?
@State private var alertType: AlertType?
@State private var purchaseState: PurchaseState?
init(isPresented: Binding<Bool>, feature: LocalProduct? = nil) {
@ -68,7 +68,7 @@ extension PaywallView {
_isPresented = isPresented
self.feature = feature
}
var body: some View {
List {
productsSection
@ -85,7 +85,7 @@ extension PaywallView {
}
}.themeAnimation(on: productManager.isRefreshingProducts)
}
private func presentedAlert(_ alertType: AlertType) -> Alert {
switch alertType {
case .purchaseFailed(let product, let error):
@ -96,7 +96,7 @@ extension PaywallView {
purchaseState = nil
}
)
case .restoreFailed(let error):
return Alert(
title: Text(L10n.Paywall.Items.Restore.title),
@ -107,7 +107,7 @@ extension PaywallView {
)
}
}
private var productsSection: some View {
Section {
if !productManager.isRefreshingProducts {
@ -134,7 +134,7 @@ extension PaywallView {
purchaseState: purchaseState
)
}
private var restoreRow: some View {
PurchaseRow(
title: L10n.Paywall.Items.Restore.title,
@ -156,7 +156,7 @@ extension PaywallView.PurchaseView {
switch result {
case .done:
isPresented = false
case .cancelled:
break
}
@ -168,7 +168,7 @@ extension PaywallView.PurchaseView {
}
}
}
private func restorePurchases() {
purchaseState = .restoring
@ -191,7 +191,7 @@ extension PaywallView.PurchaseView {
}
return productManager.product(withIdentifier: feature)
}
private var skPlatformVersion: SKProduct? {
#if targetEnvironment(macCatalyst)
productManager.product(withIdentifier: .fullVersion_macOS)
@ -258,13 +258,13 @@ private struct PurchaseRow: View {
var product: SKProduct?
let title: String
let extra: String?
let action: () -> Void
let purchaseState: PaywallView.PurchaseView.PurchaseState?
var body: some View {
VStack(alignment: .leading) {
actionButton
@ -276,7 +276,7 @@ private struct PurchaseRow: View {
}
}.padding([.top, .bottom])
}
@ViewBuilder
private var actionButton: some View {
if let product = product {
@ -285,7 +285,7 @@ private struct PurchaseRow: View {
restoreButton
}
}
private func purchaseButton(_ product: SKProduct) -> some View {
HStack {
Button(title, action: action)
@ -300,7 +300,7 @@ private struct PurchaseRow: View {
}
}
}
private var restoreButton: some View {
HStack {
Button(title, action: action)

View File

@ -27,9 +27,9 @@ import SwiftUI
struct PaywallView: View {
@ObservedObject private var productManager: ProductManager
@Binding private var isPresented: Bool
private let feature: LocalProduct?
init<MT>(modalType: Binding<MT?>, feature: LocalProduct? = nil) {

View File

@ -33,15 +33,15 @@ extension ProfileView {
@ObservedObject private var currentProfile: ObservableProfile
@Binding private var modalType: ModalType?
private var isEligibleForNetworkSettings: Bool {
productManager.isEligible(forFeature: .networkSettings)
}
private var isEligibleForTrustedNetworks: Bool {
productManager.isEligible(forFeature: .trustedNetworks)
}
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
productManager = .shared
self.currentProfile = currentProfile
@ -111,7 +111,7 @@ extension ProfileView {
Text(L10n.Global.Strings.configuration)
}
}
private var networkSettingsRow: some View {
Label(L10n.NetworkSettings.title, systemImage: themeNetworkSettingsImage)
}

View File

@ -29,7 +29,7 @@ import PassepartoutLibrary
extension ProfileView {
struct DiagnosticsSection: View {
@ObservedObject var currentProfile: ObservableProfile
var body: some View {
Section {
NavigationLink {

View File

@ -29,11 +29,11 @@ import PassepartoutLibrary
extension ProfileView {
struct ExtraSection: View {
@ObservedObject private var currentProfile: ObservableProfile
init(currentProfile: ObservableProfile) {
self.currentProfile = currentProfile
}
var body: some View {
if currentProfile.value.isProvider {
Section {

View File

@ -30,36 +30,36 @@ extension ProfileView {
struct MainMenu: View {
enum ActionSheetType: Int, Identifiable {
case uninstallVPN
case deleteProfile
var id: Int {
rawValue
}
}
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var vpnManager: VPNManager
@ObservedObject private var currentVPNState: ObservableVPNState
@ObservedObject private var currentProfile: ObservableProfile
private var header: Profile.Header {
currentProfile.value.header
}
@Binding private var modalType: ModalType?
@State private var actionSheetType: ActionSheetType?
private let uninstallVPNTitle = L10n.Global.Strings.uninstall
private let deleteProfileTitle = L10n.Global.Strings.delete
private let cancelTitle = L10n.Global.Strings.cancel
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
profileManager = .shared
vpnManager = .shared
@ -79,7 +79,7 @@ extension ProfileView {
primaryButton: .destructive(Text(uninstallVPNTitle), action: uninstallVPN),
secondaryButton: .cancel(Text(cancelTitle))
)
case .deleteProfile:
return Alert(
title: Text(deleteProfileTitle),
@ -114,7 +114,7 @@ extension ProfileView {
themeSettingsMenuImage.asSystemImage
}
}
private var isActiveProfileNotDisconnected: Bool {
profileManager.isActiveProfile(header.id) && currentVPNState.vpnStatus != .disconnected
}
@ -126,7 +126,7 @@ extension ProfileView {
Label(uninstallVPNTitle, systemImage: themeUninstallImage)
}
}
private var deleteProfileButton: some View {
DestructiveButton {
actionSheetType = .deleteProfile
@ -152,11 +152,11 @@ extension ProfileView {
extension ProfileView {
struct ReconnectButton: View {
@ObservedObject private var vpnManager: VPNManager
init() {
vpnManager = .shared
}
var body: some View {
Button {
Task { @MainActor in
@ -167,12 +167,12 @@ extension ProfileView {
}
}
}
struct ShortcutsButton: View {
@ObservedObject private var productManager: ProductManager
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
productManager = .shared
_modalType = modalType
@ -181,7 +181,7 @@ extension ProfileView {
private var isEligibleForSiri: Bool {
productManager.isEligible(forFeature: .siriShortcuts)
}
var body: some View {
Button {
presentShortcutsOrPaywall()
@ -200,14 +200,14 @@ extension ProfileView {
}
}
}
struct RenameButton: View {
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
_modalType = modalType
}
var body: some View {
Button {
modalType = .rename
@ -219,17 +219,17 @@ extension ProfileView {
struct DuplicateButton: View {
@ObservedObject private var profileManager: ProfileManager
private let header: Profile.Header
private let setAsCurrent: Bool
init(header: Profile.Header, setAsCurrent: Bool) {
profileManager = .shared
self.header = header
self.setAsCurrent = setAsCurrent
}
var body: some View {
Button {
duplicateProfile(withId: header.id)

View File

@ -29,17 +29,17 @@ import PassepartoutLibrary
extension ProfileView {
struct ProviderSection: View, ProviderProfileAvailability {
@ObservedObject var providerManager: ProviderManager
@ObservedObject private var currentProfile: ObservableProfile
var profile: Profile {
currentProfile.value
}
@State private var isProviderLocationPresented = false
@State private var isRefreshingInfrastructure = false
init(currentProfile: ObservableProfile) {
providerManager = .shared
self.currentProfile = currentProfile
@ -55,7 +55,7 @@ extension ProfileView {
}
}
}
@ViewBuilder
private var mainView: some View {
Section {
@ -116,11 +116,11 @@ extension ProfileView {
}
return themeAssetsCountryImage(code).asAssetImage
}
private var currentProviderPreset: String? {
providerManager.localizedPreset(forProfile: profile)
}
private var lastInfrastructureUpdate: String? {
providerManager.localizedInfrastructureUpdate(forProfile: profile)
}

View File

@ -29,20 +29,20 @@ import PassepartoutLibrary
extension ProfileView {
struct RenameView: View {
@Environment(\.presentationMode) private var presentationMode
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var currentProfile: ObservableProfile
@State private var newName = ""
@State private var isOverwritingExistingProfile = false
init(currentProfile: ObservableProfile) {
profileManager = .shared
self.currentProfile = currentProfile
}
var body: some View {
List {
Section {
@ -61,7 +61,7 @@ extension ProfileView {
}
}.alert(isPresented: $isOverwritingExistingProfile, content: alertOverwriteExistingProfile)
}
private func alertOverwriteExistingProfile() -> Alert {
Alert(
title: Text(L10n.Profile.Alerts.Rename.title),
@ -72,7 +72,7 @@ extension ProfileView {
secondaryButton: .cancel(Text(L10n.Global.Strings.cancel))
)
}
private func loadCurrentName() {
newName = currentProfile.value.header.name
}

View File

@ -33,7 +33,7 @@ extension ProfileView {
private let profile: Profile
@Binding private var modalType: ModalType?
private var interactiveProfile: Binding<Profile?> {
.init {
modalType == .interactiveAccount ? profile : nil
@ -41,7 +41,7 @@ extension ProfileView {
modalType = $0 != nil ? .interactiveAccount : nil
}
}
private var isActiveProfile: Bool {
profileManager.isActiveProfile(profile.id)
}
@ -51,7 +51,7 @@ extension ProfileView {
self.profile = profile
_modalType = modalType
}
var body: some View {
Section {
toggleView
@ -63,7 +63,7 @@ extension ProfileView {
.xxxThemeTruncation()
}
}
private var toggleView: some View {
VPNToggle(
profile: profile,
@ -71,7 +71,7 @@ extension ProfileView {
rateLimit: Constants.RateLimit.vpnToggle
)
}
private var statusView: some View {
HStack {
Text(L10n.Profile.Items.ConnectionStatus.caption)

View File

@ -31,13 +31,13 @@ struct ProfileView: View {
case interactiveAccount
case shortcuts
case rename
case paywallShortcuts
case paywallNetworkSettings
case paywallTrustedNetworks
var id: Int {
@ -46,17 +46,17 @@ struct ProfileView: View {
}
@ObservedObject private var currentProfile: ObservableProfile
private var isLoading: Bool {
currentProfile.isLoading
}
private var isExisting: Bool {
!currentProfile.value.isPlaceholder
}
@State private var modalType: ModalType?
init() {
currentProfile = ProfileManager.shared.currentProfile
}
@ -83,11 +83,11 @@ struct ProfileView: View {
.navigationTitle(title)
.themeSecondaryView()
}
private var title: String {
currentProfile.name
}
private var mainView: some View {
List {
if !isLoading {
@ -107,7 +107,7 @@ struct ProfileView: View {
}
}.themeAnimation(on: isLoading)
}
@ViewBuilder
private func presentedModal(_ modalType: ModalType) -> some View {
switch modalType {
@ -115,17 +115,17 @@ struct ProfileView: View {
NavigationView {
InteractiveConnectionView(profile: currentProfile.value)
}.themeGlobal()
case .shortcuts:
NavigationView {
ShortcutsView(target: currentProfile.value)
}.themeGlobal()
case .rename:
NavigationView {
RenameView(currentProfile: currentProfile)
}.themeGlobal()
case .paywallShortcuts:
NavigationView {
PaywallView(

View File

@ -30,13 +30,13 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
@ObservedObject var providerManager: ProviderManager
@ObservedObject private var currentProfile: ObservableProfile
var profile: Profile {
currentProfile.value
}
private let isEditable: Bool
private var providerName: ProviderName {
guard let name = currentProfile.value.header.providerName else {
assertionFailure("Not a provider")
@ -44,15 +44,15 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
}
return name
}
private var vpnProtocol: VPNProtocolType {
currentProfile.value.currentVPNProtocol
}
@Binding private var selectedServer: ProviderServer?
@Binding private var favoriteLocationIds: Set<String>?
@AppStorage(AppPreference.isShowingFavorites.key) private var isShowingFavorites = false
private var isShowingEmptyFavorites: Bool {
@ -61,7 +61,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
}
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
@ -69,7 +69,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
self.providerManager = providerManager
self.currentProfile = currentProfile
self.isEditable = isEditable
_selectedServer = .init {
guard let serverId = currentProfile.value.providerServerId() else {
return nil
@ -89,7 +89,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
currentProfile.value.setProviderFavoriteLocationIds($0)
}
}
var body: some View {
debugChanges()
return Group {
@ -110,7 +110,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
}
}.navigationTitle(L10n.Provider.Location.title)
}
private var mainView: some View {
// FIXME: layout, restore ScrollViewReader, but content inside it is not re-rendered on isShowingFavorites
// ScrollViewReader { scrollProxy in
@ -129,7 +129,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
private var categoriesView: some View {
ForEach(categories, content: categorySection)
}
private func categorySection(_ category: ProviderCategory) -> some View {
Section {
ForEach(filteredLocations(for: category)) { location in
@ -146,7 +146,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
!category.name.isEmpty ? Text(category.name) : nil
}
}
@ViewBuilder
private func locationRow(_ location: ProviderLocation) -> some View {
if let onlyServer = location.onlyServer {
@ -155,7 +155,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
multipleServersRow(location)
}
}
private func multipleServersRow(_ location: ProviderLocation) -> some View {
NavigationLink(destination: {
ServerListView(
@ -180,7 +180,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
)
}
}
private var emptyFavoritesSection: some View {
Section {
} footer: {
@ -204,7 +204,7 @@ extension ProviderLocationView {
private func server(withId serverId: String) -> ProviderServer? {
providerManager.server(withId: serverId)
}
private var categories: [ProviderCategory] {
providerManager.categories(providerName, vpnProtocol: vpnProtocol)
.filter {
@ -223,11 +223,11 @@ extension ProviderLocationView {
}
return locations.sorted()
}
private func isFavoriteLocation(_ location: ProviderLocation) -> Bool {
favoriteLocationIds?.contains(location.id) ?? false
}
private func toggleFavoriteLocation(_ location: ProviderLocation) {
if !isFavoriteLocation(location) {
if favoriteLocationIds == nil {
@ -248,7 +248,7 @@ extension ProviderLocationView {
let location: ProviderLocation
let selectedLocationId: String?
var body: some View {
HStack {
themeAssetsCountryImage(location.countryCode).asAssetImage
@ -269,11 +269,11 @@ extension ProviderLocationView {
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

View File

@ -34,13 +34,13 @@ struct ProviderPresetView: View {
@ObservedObject private var currentProfile: ObservableProfile
private var server: ProviderServer?
@Binding private var selectedPreset: ProviderServer.Preset?
// XXX: do not escape mutating 'self', use constant providerManager
init(currentProfile: ObservableProfile) {
let providerManager: ProviderManager = .shared
self.providerManager = providerManager
self.currentProfile = currentProfile
@ -61,7 +61,7 @@ struct ProviderPresetView: View {
currentProfile.value.setProviderPreset(preset)
}
}
var body: some View {
List {
ForEach(availablePresets, id: \.id, content: presetSection)
@ -77,7 +77,7 @@ struct ProviderPresetView: View {
presetSelectionRow(preset)
}
NavigationLink(L10n.Endpoint.Advanced.title) {
// TODO: WireGuard, preset assumes OpenVPN (read current protocol instead)
preset.openVPNConfiguration.map {
EndpointAdvancedView.OpenVPNView(
@ -91,7 +91,7 @@ struct ProviderPresetView: View {
Text(preset.name)
}
}
private func presetSelectionRow(_ preset: ProviderServer.Preset) -> some View {
Text(preset.comment)
.withTrailingCheckmark(when: preset.id == selectedPreset?.id)

View File

@ -29,15 +29,15 @@ import PassepartoutLibrary
struct ReportIssueView: View {
@Binding private var isPresented: Bool
private let toRecipients: [String]
private let subject: String
private let messageBody: String
private let attachments: [MailComposerView.Attachment]
init(
isPresented: Binding<Bool>,
vpnProtocol: VPNProtocolType,
@ -46,7 +46,7 @@ struct ReportIssueView: View {
lastUpdate: Date? = nil
) {
_isPresented = isPresented
toRecipients = [Unlocalized.Issues.recipient]
subject = Unlocalized.Issues.subject
@ -77,7 +77,7 @@ struct ReportIssueView: View {
}
self.attachments = attachments
}
var body: some View {
MailComposerView(
isPresented: $isPresented,

View File

@ -30,9 +30,9 @@ struct SettingsView: View {
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var productManager: ProductManager
@Environment(\.presentationMode) private var presentationMode
// private var isTestBuild: Bool {
// Constants.App.isBeta || Constants.InApp.appType == .beta
// }
@ -45,7 +45,7 @@ struct SettingsView: View {
profileManager = .shared
productManager = .shared
}
var body: some View {
List {
aboutSection
@ -54,7 +54,7 @@ struct SettingsView: View {
}.themeSecondaryView()
.navigationTitle(L10n.Settings.title)
}
private var aboutSection: some View {
Section {
NavigationLink {

View File

@ -30,11 +30,11 @@ import PassepartoutLibrary
extension ShortcutsView {
struct AddView: View {
@ObservedObject private var providerManager: ProviderManager
@StateObject private var pendingProfile = ObservableProfile()
private let target: Profile
@Binding private var pendingShortcut: INShortcut?
@State private var isPresentingProviderLocation = false
@ -44,7 +44,7 @@ extension ShortcutsView {
self.target = target
_pendingShortcut = pendingShortcut
}
var body: some View {
ZStack {
hiddenProviderLocationLink
@ -82,7 +82,7 @@ extension ShortcutsView {
}
}
}
private var hiddenProviderLocationLink: some View {
NavigationLink("", isActive: $isPresentingProviderLocation) {
ProviderLocationView(
@ -106,7 +106,7 @@ extension ShortcutsView.AddView {
}
}
}
private func addConnect(_ header: Profile.Header) {
pendingShortcut = INShortcut(intent: IntentDispatcher.intentConnect(
header: header

View File

@ -30,35 +30,35 @@ import PassepartoutLibrary
struct ShortcutsView: View {
enum ModalType: Identifiable {
case edit(shortcut: Shortcut)
case add(shortcut: INShortcut)
// XXX: alert ids
var id: Int {
switch self {
case .edit: return 1
case .add: return 2
}
}
}
@StateObject private var intentsManager = IntentsManager()
@Environment(\.presentationMode) private var presentationMode
private let target: Profile
@State private var modalType: ModalType?
@State private var isNavigationPresented = false
@State private var pendingShortcut: INShortcut?
init(target: Profile) {
self.target = target
}
var body: some View {
List {
if !intentsManager.shortcuts.isEmpty {
@ -69,16 +69,16 @@ struct ShortcutsView: View {
themeCloseItem(presentationMode: presentationMode)
}.sheet(item: $modalType, content: presentedModal)
.themeAnimation(on: intentsManager.isReloadingShortcuts)
// IntentsUI
.onReceive(intentsManager.shouldDismissIntentView) { _ in
modalType = nil
}
.navigationTitle(Unlocalized.Other.siri)
.themeSecondaryView()
}
private var shortcutsSection: some View {
Section {
ForEach(relevantShortcuts, content: rowView)
@ -86,13 +86,13 @@ struct ShortcutsView: View {
Text(L10n.Shortcuts.Edit.Sections.All.header)
}
}
private var relevantShortcuts: [Shortcut] {
intentsManager.shortcuts.values.filter {
$0.isRelevant(to: target)
}.sorted()
}
private var addSection: some View {
Section {
NavigationLink(isActive: $isNavigationPresented) {
@ -116,7 +116,7 @@ struct ShortcutsView: View {
shortcut: shortcut,
delegate: intentsManager
)
case .add(let shortcut):
IntentAddView(
shortcut: shortcut,
@ -124,7 +124,7 @@ struct ShortcutsView: View {
)
}
}
private func rowView(forShortcut vs: Shortcut) -> some View {
Button {
presentEditShortcut(vs)
@ -143,7 +143,7 @@ struct ShortcutsView: View {
presentAddShortcut(pendingShortcut)
}
}
private func presentEditShortcut(_ shortcut: Shortcut) {
modalType = .edit(shortcut: shortcut)
}

View File

@ -28,14 +28,14 @@ import PassepartoutLibrary
struct VPNStatusText: View {
@ObservedObject private var currentVPNState: ObservableVPNState
let isActiveProfile: Bool
init(isActiveProfile: Bool) {
currentVPNState = .shared
self.isActiveProfile = isActiveProfile
}
var body: some View {
Text(statusText)
}

View File

@ -36,11 +36,11 @@ struct VPNToggle: View {
@ObservedObject private var productManager: ProductManager
private let profile: Profile
@Binding private var interactiveProfile: Profile?
private let rateLimit: Int
private var isEnabled: Binding<Bool> {
.init {
isActiveProfile && currentVPNState.isEnabled && !shouldPromptForAccount
@ -56,11 +56,11 @@ struct VPNToggle: View {
enableVPN()
}
}
private var isActiveProfile: Bool {
profileManager.isActiveProfile(profile.id)
}
private var shouldPromptForAccount: Bool {
profile.account.authenticationMethod == .interactive && (currentVPNState.vpnStatus == .disconnecting || currentVPNState.vpnStatus == .disconnected)
}
@ -68,9 +68,9 @@ struct VPNToggle: View {
private var isEligibleForSiri: Bool {
productManager.isEligible(forFeature: .siriShortcuts)
}
@State private var canToggle = true
init(profile: Profile, interactiveProfile: Binding<Profile?>, rateLimit: Int) {
profileManager = .shared
vpnManager = .shared
@ -86,7 +86,7 @@ struct VPNToggle: View {
.disabled(!canToggle)
.themeAnimation(on: currentVPNState.isEnabled)
}
private func enableVPN() {
Task { @MainActor in
canToggle = false
@ -101,7 +101,7 @@ struct VPNToggle: View {
}
}
}
private func disableVPN() {
Task { @MainActor in
canToggle = false
@ -109,7 +109,7 @@ struct VPNToggle: View {
canToggle = true
}
}
private func donateIntents(withProfile profile: Profile) {
// eligibility: donate intents if eligible for Siri

View File

@ -52,7 +52,7 @@ extension View {
self
}
}
func withLeadingLabel(_ text: String, color: Color? = nil, image: String) -> some View {
HStack {
Label(text, image: image)
@ -80,7 +80,7 @@ extension View {
}
}
}
func withTrailingCheckmark(when condition: Bool) -> some View {
HStack {
self
@ -91,7 +91,7 @@ extension View {
}
}
}
func withTrailingProgress(when condition: Bool) -> some View {
HStack {
self
@ -145,7 +145,7 @@ private extension View {
}
private struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
var callback: (UIWindow?) -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView()

View File

@ -30,11 +30,11 @@ import PassepartoutLibrary
@MainActor
class CoreContext {
let store: KeyValueStore
private let profilesPersistence: Persistence
private let providersPersistence: Persistence
var urlsForProfiles: [URL]? {
profilesPersistence.containerURLs
}
@ -44,18 +44,18 @@ class CoreContext {
}
let upgradeManager: UpgradeManager
let providerManager: ProviderManager
let profileManager: ProfileManager
let vpnManager: VPNManager
private var cancellables: Set<AnyCancellable> = []
init(store: KeyValueStore) {
self.store = store
let persistenceManager = PersistenceManager(store: store)
profilesPersistence = persistenceManager.profilesPersistence(
withName: Constants.Persistence.profilesContainerName
@ -106,12 +106,12 @@ class CoreContext {
providerManager: providerManager,
strategy: strategy
)
// post
configureObjects()
}
private func configureObjects() {
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
vpnManager.tunnelLogPath = Constants.Log.Tunnel.path

View File

@ -41,16 +41,16 @@ extension PassepartoutError {
switch self {
case .missingProfile:
return V.missingProfile
case .missingAccount:
return V.missingAccount
case .missingProviderServer:
return V.missingProviderServer
case .missingProviderPreset:
return V.missingProviderPreset
default:
return nil
}
@ -103,7 +103,7 @@ extension Network.Choice {
switch self {
case .automatic:
return L10n.Global.Strings.automatic
case .manual:
return L10n.Global.Strings.manual
}
@ -115,7 +115,7 @@ extension Network.DNSSettings.ConfigurationType {
switch self {
case .plain:
return Unlocalized.DNS.plain
case .https:
return Unlocalized.Network.https
@ -133,10 +133,10 @@ extension Network.ProxySettings.ConfigurationType {
switch self {
case .manual:
return L10n.Global.Strings.manual
case .pac:
return Unlocalized.Network.proxyAutoConfiguration
case .disabled:
return L10n.Global.Strings.disabled
}

View File

@ -43,10 +43,10 @@ extension OpenVPN.CompressionFraming {
switch self {
case .disabled:
return L10n.Global.Strings.disabled
case .compLZO:
return Unlocalized.OpenVPN.compLZO
case .compress, .compressV2:
return Unlocalized.OpenVPN.compress
}
@ -59,10 +59,10 @@ extension OpenVPN.CompressionAlgorithm {
switch self {
case .disabled:
return L10n.Global.Strings.disabled
case .LZO:
return Unlocalized.OpenVPN.lzo
case .other:
return V.CompressionAlgorithm.Value.other
}
@ -90,26 +90,26 @@ extension OpenVPN.XORMethod {
switch self {
case .xormask:
return Unlocalized.OpenVPN.XOR.xormask.rawValue
case .xorptrpos:
return Unlocalized.OpenVPN.XOR.xorptrpos.rawValue
case .reverse:
return Unlocalized.OpenVPN.XOR.reverse.rawValue
case .obfuscate:
return Unlocalized.OpenVPN.XOR.obfuscate.rawValue
}
}
var localizedLongDescription: String {
switch self {
case .xormask(let mask):
return "\(localizedDescription) \(mask.toHex())"
case .obfuscate(let mask):
return "\(localizedDescription) \(mask.toHex())"
default:
return localizedDescription
}
@ -139,7 +139,7 @@ extension Bool {
let V = L10n.Global.Strings.self
return self ? V.enabled : V.disabled
}
var localizedDescriptionAsRandomizeHostnames: String {
let V = L10n.Global.Strings.self
return self ? V.enabled : V.disabled
@ -151,10 +151,10 @@ extension OpenVPN.PullMask {
switch self {
case .routes:
return L10n.Endpoint.Advanced.Openvpn.Items.Route.caption
case .dns:
return Unlocalized.Network.dns
case .proxy:
return L10n.Global.Strings.proxy
}

View File

@ -33,7 +33,7 @@ extension ProviderManager {
}
return profile.providerPreset(server)?.localizedDescription
}
func localizedInfrastructureUpdate(forProfile profile: Profile) -> String? {
guard let providerName = profile.header.providerName else {
return nil

View File

@ -35,13 +35,13 @@ extension VPNStatus {
switch self {
case .connecting:
return L10n.Tunnelkit.Vpn.connecting
case .connected:
return L10n.Tunnelkit.Vpn.active
case .disconnecting:
return L10n.Tunnelkit.Vpn.disconnecting
case .disconnected:
return L10n.Tunnelkit.Vpn.inactive
}
@ -121,37 +121,37 @@ extension Error {
}
return localizedDescription
}
private func ovpnErrorDescription(_ error: OpenVPNProviderError) -> String? {
let V = L10n.Tunnelkit.Errors.Vpn.self
switch error {
case .socketActivity, .timeout:
return V.timeout
case .dnsFailure:
return V.dns
case .tlsInitialization, .tlsServerVerification, .tlsHandshake:
return V.tls
case .authentication:
return V.auth
case .encryptionInitialization, .encryptionData:
return V.encryption
case .serverCompression, .lzo:
return V.compression
case .networkChanged:
return V.network
case .routing:
return V.routing
case .gatewayUnattainable:
return V.gateway
case .serverShutdown:
return V.shutdown
@ -184,7 +184,7 @@ extension Error {
pp_log.error("Could not parse configuration URL: \(localizedDescription)")
return L10n.Tunnelkit.Errors.parsing(localizedDescription)
}
private func ovpnErrorDescription(_ error: OpenVPN.ConfigurationError) -> String {
let V = L10n.Tunnelkit.Errors.Openvpn.self
switch error {
@ -203,7 +203,7 @@ extension Error {
case .missingConfiguration(let option):
pp_log.error("Could not parse configuration URL: missing configuration, \(option)")
return V.requiredOption(option)
case .unsupportedConfiguration(var option):
if option.contains("external") {
option.append(" (see FAQ)")

View File

@ -28,24 +28,24 @@ import PassepartoutLibrary
enum Unlocalized {
static let appName = Constants.Global.appName
enum Placeholders {
static let empty = ""
static let address = "0.0.0.0"
static let port = "8080"
static let hostname = "example.com"
static let dohURL = "https://example.com/dns-query"
static let dotServerName = hostname
static let dnsAddress = address
static let dnsDomain = hostname
static let pacURL = "https://proxy/auto-conf"
static let proxyBypassDomain = hostname
@ -54,24 +54,24 @@ enum Unlocalized {
enum DNS {
static let plain = "Cleartext"
}
enum Keychain {
static func passwordLabel(_ profileName: String, vpnProtocol: VPNProtocolType) -> String {
"\(Constants.Global.appName): \(profileName) (\(vpnProtocol.description))"
}
}
enum Issues {
static let recipient = "issues@\(Constants.Domain.name)"
static let subject = "\(appName) - Report issue"
static func body(_ description: String, _ metadata: String) -> String {
"Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
}
static let template = "description of the issue: "
static let maxLogBytes = UInt64(20000)
enum Filenames {
@ -81,13 +81,13 @@ enum Unlocalized {
let iso = fmt.string(from: Date())
return "debug-\(iso).txt"
}
static let configuration = "profile.ovpn"
// static let configuration = "profile.ovpn.txt"
static let template = "description of the issue: "
}
enum MIME {
static let debugLog = "text/plain"
@ -98,9 +98,9 @@ enum Unlocalized {
enum Social {
static let reddit = "Reddit"
private static let twitterHashtags = ["OpenVPN", "WireGuard", "iOS", "macOS"]
static func twitterIntent(withMessage message: String) -> URL {
var text = message
for ht in twitterHashtags {
@ -121,7 +121,7 @@ enum Unlocalized {
static let recipient = "translate@\(Constants.Domain.name)"
static let subject = "\(appName) - Translations"
static func body(_ description: String) -> String {
"Hi,\n\n\(description)\n\nRegards"
}
@ -148,9 +148,9 @@ enum Unlocalized {
enum Credits {
typealias License = (String, String, URL)
typealias Notice = (String, String)
static let author = "Davide De Rosa"
static let licenses: [License] = [(
@ -178,7 +178,7 @@ enum Unlocalized {
"MIT",
URL(string: "https://raw.githubusercontent.com/SwiftyBeaver/SwiftyBeaver/master/LICENSE")!
)]
static let notices: [Notice] = [(
"Circle Icons",
"The logo is taken from the awesome Circle Icons set by Nick Roach."
@ -196,63 +196,63 @@ enum Unlocalized {
enum About {
static let github = "GitHub"
static let readme = "README"
static let changelog = "CHANGELOG"
static let faq = "FAQ"
}
enum VPN {
static let vpn = "VPN"
static let certificateAuthority = "CA"
static let xor = "XOR"
}
enum OpenVPN {
static let compLZO = "--comp-lzo"
static let compress = "--compress"
static let lzo = "LZO"
enum XOR: String {
case xormask
case xorptrpos
case reverse
case obfuscate
}
}
enum Network {
static let dns = "DNS"
static let tls = "TLS"
static let https = "HTTPS"
static let url = "URL"
static let mtu = "MTU"
static let ipv4 = "IPv4"
static let ipv6 = "IPv6"
static let ssid = "SSID"
static let proxyAutoConfiguration = "PAC"
}
enum Other {
static let siri = "Siri"
static let totp = "TOTP"
}
}

View File

@ -28,8 +28,8 @@ import Foundation
@objc(MacBridge)
public protocol MacBridge: NSObjectProtocol {
init()
var utils: MacUtils { get }
var menu: MacMenu { get }
}

View File

@ -28,7 +28,7 @@ import Foundation
@objc
public protocol MacUtils {
var isStartedByLauncher: Bool { get }
func sendAppToBackground()
}

View File

@ -28,13 +28,13 @@ import Foundation
@objc(LightProfile)
public protocol LightProfile {
var id: UUID { get }
var name: String { get }
var vpnProtocol: String { get }
var isActive: Bool { get }
var providerName: String? { get }
var providerServer: LightProviderServer? { get }
@ -50,13 +50,13 @@ extension LightProfile {
@objc
public protocol LightProfileManager {
var hasProfiles: Bool { get }
var profiles: [LightProfile] { get }
var activeProfileId: UUID? { get }
var activeProfileName: String? { get }
var delegate: LightProfileManagerDelegate? { get set }
}

View File

@ -39,20 +39,20 @@ public protocol LightProviderLocation {
var id: String { get }
var countryCode: String { get }
var servers: [LightProviderServer] { get }
}
@objc(LightProviderServer)
public protocol LightProviderServer {
var description: String { get }
var longDescription: String { get }
var categoryName: String { get }
var locationId: String { get }
var serverId: String { get }
}
@ -60,7 +60,7 @@ public protocol LightProviderServer {
@objc
public protocol LightProviderManager {
var delegate: LightProviderManagerDelegate? { get set }
func categories(_ name: String, vpnProtocol: String) -> [LightProviderCategory]
func downloadIfNeeded(_ name: String, vpnProtocol: String)

View File

@ -28,6 +28,6 @@ import Foundation
@objc
public protocol LightUtils {
var launchesOnLogin: Bool { get set }
func requestScene()
}

View File

@ -28,11 +28,11 @@ import Foundation
@objc(LightVPNStatus)
public enum LightVPNStatus: Int {
case connecting
case connected
case disconnecting
case disconnected
}
@ -42,19 +42,19 @@ public protocol LightVPNManager {
var isEnabled: Bool { get }
var vpnStatus: LightVPNStatus { get }
func connect(with profileId: UUID)
func connect(with profileId: UUID, to serverId: String)
func disconnect()
func toggle()
func reconnect()
func addDelegate(_ delegate: LightVPNManagerDelegate, withIdentifier identifier: String)
func removeDelegate(withIdentifier identifier: String)
}

View File

@ -28,10 +28,10 @@ import Foundation
@objc(MenuBuilder)
public protocol MenuBuilder: NSObjectProtocol {
weak var delegate: MenuDelegate? { get set }
init()
func sendAppToBackground()
func buildMenu()
}

View File

@ -28,7 +28,7 @@ import AppKit
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private let appURL = Constants.Launcher.appURL
private var isAppRunning: Bool {
NSWorkspace.shared.runningApplications.contains {
$0.bundleIdentifier == Constants.Launcher.appId
@ -40,12 +40,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
NSApp.terminate(self)
return
}
let cfg = NSWorkspace.OpenConfiguration()
cfg.hides = true
cfg.activates = false
cfg.addsToRecentItems = false
NSWorkspace.shared.openApplication(at: appURL, configuration: cfg) { app, error in
NSWorkspace.shared.openApplication(at: appURL, configuration: cfg) { _, error in
if let error = error {
NSLog("Unable to launch main app: \(error)")
return

View File

@ -30,7 +30,7 @@ extension Constants {
static var bundle: Bundle {
Bundle(for: PassepartoutMac.self)
}
static let appLauncherId: String = bundleConfig("launcher_id", in: bundle)
}
}

View File

@ -52,7 +52,7 @@ extension LightVPNStatus {
switch self {
case .connected, .disconnected:
resourceName = "StatusActive"
case .connecting, .disconnecting:
resourceName = "StatusPending"
}
@ -61,12 +61,12 @@ extension LightVPNStatus {
}
return image
}
var imageAlpha: Double {
switch self {
case .disconnected:
return 0.5
default:
return 1.0
}

View File

@ -27,14 +27,14 @@ import Foundation
class DefaultMacMenu: MacMenu {
weak var delegate: MacMenuDelegate?
private lazy var menu: PassepartoutMenu = {
guard let delegate = delegate else {
fatalError("Must set MacMenu.delegate")
}
return PassepartoutMenu(macMenuDelegate: delegate)
}()
func install() {
menu.install()
}

Some files were not shown because too many files have changed in this diff Show More