Persist managers state to generic key-value store
Move all persisted state out of AppManager to where it really belongs. To do that, inject a shared KeyValueStore object into managers that need to persist part of their state in a strongly typed manner. Below are persisted states: - PersistenceManager - persistenceAuthor - ProfileManager - activeProfileId - UpgradeManager (formerly AppManager) - didMigrateToV2 (migrate former value) - VPNManager - tunnelLogFormat - masksPrivateData A similar approach is used for app-specific preferences, by using a strongly typed enum (AppPreference) together with SwiftUI @AppStorage property wrapper. Worth moving logging logic into a specific LogManager. Finally, drop any former view dependency on AppManager, as states are now accessed through specific managers.
This commit is contained in:
parent
127ba28a8c
commit
14b42fbea5
|
@ -16,6 +16,7 @@
|
|||
0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */; };
|
||||
0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E0C072B236087A100155AAC /* InfoPlist.strings */; };
|
||||
0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; };
|
||||
0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293850285A70AC002A6E0E /* AppPreference.swift */; };
|
||||
0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */; };
|
||||
0E2A8D4F27B04BBA00207D04 /* OrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */; };
|
||||
0E2AC24522EC3AC10037B4B0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */; };
|
||||
|
@ -47,7 +48,6 @@
|
|||
0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */; };
|
||||
0E5349C627C176C200C71BB3 /* EndpointView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */; };
|
||||
0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */; };
|
||||
0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E53E63627E34FE2001D4902 /* AppContext.swift */; };
|
||||
0E5683B927C2825D00EAF1CD /* DiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */; };
|
||||
0E6059CB27FCC5DE003F4063 /* Flags.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C827FCC5DD003F4063 /* Flags.xcassets */; };
|
||||
0E6059CC27FCC5DE003F4063 /* Providers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C927FCC5DE003F4063 /* Providers.xcassets */; };
|
||||
|
@ -123,6 +123,7 @@
|
|||
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; };
|
||||
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; };
|
||||
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; };
|
||||
0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2CC02285AFED800E501D5 /* AppContext.swift */; };
|
||||
0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -202,6 +203,7 @@
|
|||
0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = "<group>"; };
|
||||
0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
|
||||
0E293850285A70AC002A6E0E /* AppPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = "<group>"; };
|
||||
0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = "<group>"; };
|
||||
0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizerView.swift; sourceTree = "<group>"; };
|
||||
0E2AC24422EC3AC10037B4B0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||
|
@ -232,7 +234,6 @@
|
|||
0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledPicker.swift; sourceTree = "<group>"; };
|
||||
0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+OpenVPN.swift"; sourceTree = "<group>"; };
|
||||
0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+WireGuard.swift"; sourceTree = "<group>"; };
|
||||
0E53E63627E34FE2001D4902 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = "<group>"; };
|
||||
0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsView.swift; sourceTree = "<group>"; };
|
||||
0E57F63820C83FC5008323CF /* Passepartout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Passepartout.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0E57F64720C83FC7008323CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -341,6 +342,7 @@
|
|||
0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = "<group>"; };
|
||||
0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = "<group>"; };
|
||||
0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = "<group>"; };
|
||||
0EF2CC02285AFED800E501D5 /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = "<group>"; };
|
||||
0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -565,7 +567,8 @@
|
|||
0EB17EA027D2263700D473B5 /* Constants */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E53E63627E34FE2001D4902 /* AppContext.swift */,
|
||||
0EF2CC02285AFED800E501D5 /* AppContext.swift */,
|
||||
0E293850285A70AC002A6E0E /* AppPreference.swift */,
|
||||
0EB17EA527D2263700D473B5 /* Constants+Extensions.swift */,
|
||||
0E6059CE27FCC618003F4063 /* SwiftGen+Assets.swift */,
|
||||
0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */,
|
||||
|
@ -907,7 +910,6 @@
|
|||
0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */,
|
||||
0E3CD47F280DA14B007075C0 /* AddProfileMenu.swift in Sources */,
|
||||
0EB17EAA27D226C900D473B5 /* Constants+Extensions.swift in Sources */,
|
||||
0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */,
|
||||
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */,
|
||||
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
|
||||
0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */,
|
||||
|
@ -930,8 +932,10 @@
|
|||
0E71ACEF27C106B500F85C4B /* ProviderPresetView.swift in Sources */,
|
||||
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */,
|
||||
0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */,
|
||||
0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */,
|
||||
0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */,
|
||||
0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */,
|
||||
0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */,
|
||||
0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */,
|
||||
0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */,
|
||||
0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */,
|
||||
|
|
|
@ -33,6 +33,8 @@ import PassepartoutServices
|
|||
class AppContext {
|
||||
static let shared = AppContext()
|
||||
|
||||
private let logManager: LogManager
|
||||
|
||||
private let profilesPersistence: Persistence
|
||||
|
||||
private let providersPersistence: Persistence
|
||||
|
@ -45,7 +47,7 @@ class AppContext {
|
|||
providersPersistence.containerURLs
|
||||
}
|
||||
|
||||
let appManager: AppManager
|
||||
let upgradeManager: UpgradeManager
|
||||
|
||||
let providerManager: ProviderManager
|
||||
|
||||
|
@ -62,18 +64,15 @@ class AppContext {
|
|||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
private init() {
|
||||
let store = UserDefaultsStore(defaults: .standard)
|
||||
|
||||
// core
|
||||
logManager = LogManager(logFile: Constants.Log.appFileURL)
|
||||
logManager.logLevel = Constants.Log.logLevel
|
||||
logManager.logFormat = Constants.Log.appLogFormat
|
||||
logManager.configureLogging()
|
||||
pp_log.info("Logging to: \(logManager.logFile!)")
|
||||
|
||||
appManager = AppManager()
|
||||
appManager.logLevel = Constants.Log.logLevel
|
||||
appManager.logFile = Constants.Log.appFileURL
|
||||
appManager.logFormat = Constants.Log.appLogFormat
|
||||
appManager.tunnelLogFormat = Constants.Log.tunnelLogFormat
|
||||
appManager.configureLogging()
|
||||
pp_log.info("Logging to: \(appManager.logFile!)")
|
||||
|
||||
let persistenceManager = PersistenceManager(author: appManager.persistenceAuthor)
|
||||
let persistenceManager = PersistenceManager(store: store)
|
||||
profilesPersistence = persistenceManager.profilesPersistence(
|
||||
withName: Constants.Persistence.profilesContainerName
|
||||
)
|
||||
|
@ -81,6 +80,8 @@ class AppContext {
|
|||
withName: Constants.Persistence.providersContainerName
|
||||
)
|
||||
|
||||
upgradeManager = UpgradeManager(store: store)
|
||||
|
||||
providerManager = ProviderManager(
|
||||
appBuild: Constants.Global.appBuildNumber,
|
||||
bundleServices: DefaultWebServices.bundledServices(
|
||||
|
@ -95,6 +96,7 @@ class AppContext {
|
|||
)
|
||||
|
||||
profileManager = ProfileManager(
|
||||
store: store,
|
||||
providerManager: providerManager,
|
||||
appGroup: Constants.App.appGroupId,
|
||||
keychainLabel: Unlocalized.Keychain.passwordLabel,
|
||||
|
@ -112,7 +114,7 @@ class AppContext {
|
|||
)
|
||||
#endif
|
||||
vpnManager = VPNManager(
|
||||
appManager: appManager,
|
||||
store: store,
|
||||
profileManager: profileManager,
|
||||
providerManager: providerManager,
|
||||
strategy: strategy
|
||||
|
@ -137,14 +139,11 @@ class AppContext {
|
|||
// core
|
||||
|
||||
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
|
||||
vpnManager.tunnelLogFormat = Constants.Log.tunnelLogFormat
|
||||
vpnManager.isOnDemandRulesSupported = {
|
||||
self.isEligibleForOnDemandRules()
|
||||
}
|
||||
|
||||
if let activeProfileId = appManager.activeProfileId {
|
||||
profileManager.setActiveProfileId(activeProfileId)
|
||||
}
|
||||
|
||||
profileManager.observeUpdates()
|
||||
vpnManager.observeUpdates()
|
||||
|
||||
|
@ -180,8 +179,8 @@ class AppContext {
|
|||
}
|
||||
}
|
||||
|
||||
extension AppManager {
|
||||
static let shared = AppContext.shared.appManager
|
||||
extension UpgradeManager {
|
||||
static let shared = AppContext.shared.upgradeManager
|
||||
}
|
||||
|
||||
extension ProfileManager {
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// AppPreference.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 6/15/22.
|
||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PassepartoutUtils
|
||||
|
||||
enum AppPreference: String, KeyStoreDomainLocation {
|
||||
case isShowingFavorites
|
||||
|
||||
case didHandleSubreddit
|
||||
|
||||
var domain: String {
|
||||
"App"
|
||||
}
|
||||
}
|
|
@ -37,8 +37,6 @@ extension DiagnosticsView {
|
|||
}
|
||||
}
|
||||
|
||||
@ObservedObject private var appManager: AppManager
|
||||
|
||||
@ObservedObject private var providerManager: ProviderManager
|
||||
|
||||
@ObservedObject private var vpnManager: VPNManager
|
||||
|
@ -62,7 +60,6 @@ extension DiagnosticsView {
|
|||
private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval
|
||||
|
||||
init(providerName: ProviderName?) {
|
||||
appManager = .shared
|
||||
providerManager = .shared
|
||||
vpnManager = .shared
|
||||
currentVPNState = .shared
|
||||
|
@ -120,7 +117,7 @@ extension DiagnosticsView {
|
|||
)
|
||||
}
|
||||
}.disabled(url == nil)
|
||||
Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $appManager.masksPrivateData)
|
||||
Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $vpnManager.masksPrivateData)
|
||||
} footer: {
|
||||
Text(L10n.Diagnostics.Sections.DebugLog.footer)
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ extension OrganizerView {
|
|||
|
||||
private func performMigrationsIfNeeded() {
|
||||
Task {
|
||||
AppManager.shared.doMigrations(profileManager)
|
||||
UpgradeManager.shared.doMigrations(profileManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ struct OrganizerView: View {
|
|||
|
||||
@State private var isHostFileImporterPresented = false
|
||||
|
||||
@AppStorage(AppManager.DefaultKey.didHandleSubreddit.rawValue) var didHandleSubreddit = false
|
||||
@AppStorage(AppPreference.didHandleSubreddit.key) private var didHandleSubreddit = false
|
||||
|
||||
private let hostFileTypes = Constants.URLs.filetypes
|
||||
|
||||
|
|
|
@ -29,8 +29,6 @@ import PassepartoutCore
|
|||
struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||
@ObservedObject var providerManager: ProviderManager
|
||||
|
||||
@ObservedObject private var appManager: AppManager
|
||||
|
||||
@ObservedObject private var currentProfile: ObservableProfile
|
||||
|
||||
var profile: Profile {
|
||||
|
@ -55,7 +53,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
|||
|
||||
@Binding private var favoriteLocationIds: Set<String>?
|
||||
|
||||
@AppStorage(AppManager.DefaultKey.isShowingFavorites.rawValue) private var isShowingFavorites = false
|
||||
@AppStorage(AppPreference.isShowingFavorites.key) private var isShowingFavorites = false
|
||||
|
||||
private var isShowingEmptyFavorites: Bool {
|
||||
guard isShowingFavorites else {
|
||||
|
@ -68,7 +66,6 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
|||
init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding<Bool>) {
|
||||
let providerManager: ProviderManager = .shared
|
||||
|
||||
appManager = .shared
|
||||
self.providerManager = providerManager
|
||||
self.currentProfile = currentProfile
|
||||
self.isEditable = isEditable
|
||||
|
|
|
@ -27,8 +27,6 @@ import SwiftUI
|
|||
import PassepartoutCore
|
||||
|
||||
struct VPNToggle: View {
|
||||
@ObservedObject private var appManager: AppManager
|
||||
|
||||
@ObservedObject private var profileManager: ProfileManager
|
||||
|
||||
@ObservedObject private var vpnManager: VPNManager
|
||||
|
@ -64,7 +62,6 @@ struct VPNToggle: View {
|
|||
@State private var canToggle = true
|
||||
|
||||
init(profileId: UUID, rateLimit: Int) {
|
||||
appManager = .shared
|
||||
profileManager = .shared
|
||||
vpnManager = .shared
|
||||
currentVPNState = .shared
|
||||
|
@ -87,10 +84,6 @@ struct VPNToggle: View {
|
|||
}
|
||||
do {
|
||||
let profile = try await vpnManager.connect(with: profileId)
|
||||
|
||||
// IMPORTANT: save immediately to keep in sync with VPN status
|
||||
appManager.activeProfileId = profileId
|
||||
|
||||
donateIntents(withProfile: profile)
|
||||
} catch {
|
||||
pp_log.warning("Unable to connect to profile \(profileId): \(error)")
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
//
|
||||
// AppManager.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 2/8/22.
|
||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import SwiftyBeaver
|
||||
|
||||
@MainActor
|
||||
public class AppManager: ObservableObject {
|
||||
public enum DefaultKey: String {
|
||||
case activeProfileId
|
||||
|
||||
case launchesOnLogin
|
||||
|
||||
case isShowingFavorites
|
||||
|
||||
case confirmsQuit
|
||||
|
||||
case logFormat
|
||||
|
||||
case tunnelLogFormat
|
||||
|
||||
case masksPrivateData
|
||||
|
||||
case didHandleSubreddit
|
||||
|
||||
// internal use
|
||||
|
||||
case persistenceAuthor
|
||||
|
||||
case didMigrateToV2
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults = .standard
|
||||
|
||||
public var logLevel: SwiftyBeaver.Level = .info
|
||||
|
||||
public var logFile: URL?
|
||||
|
||||
// MARK: State
|
||||
|
||||
@Published public private(set) var isDoingMigrations = true
|
||||
|
||||
public init() {
|
||||
defaults.register(keyedDefaults: [
|
||||
.activeProfileId: nil,
|
||||
.launchesOnLogin: false,
|
||||
.isShowingFavorites: false,
|
||||
.confirmsQuit: true,
|
||||
.logFormat: nil,
|
||||
.tunnelLogFormat: nil,
|
||||
.masksPrivateData: true,
|
||||
.didHandleSubreddit: false,
|
||||
//
|
||||
.didMigrateToV2: false
|
||||
])
|
||||
|
||||
// set once
|
||||
if persistenceAuthor == nil {
|
||||
persistenceAuthor = UUID().uuidString
|
||||
}
|
||||
}
|
||||
|
||||
public func configureLogging() {
|
||||
let console = ConsoleDestination()
|
||||
console.minLevel = logLevel
|
||||
// console.useNSLog = true
|
||||
if let logFormat = logFormat {
|
||||
console.format = logFormat
|
||||
}
|
||||
SwiftyBeaver.addDestination(console)
|
||||
|
||||
if let fileURL = logFile {
|
||||
let file = FileDestination()
|
||||
file.minLevel = logLevel
|
||||
file.logFileURL = fileURL
|
||||
if let logFormat = logFormat {
|
||||
file.format = logFormat
|
||||
}
|
||||
_ = file.deleteLogFile()
|
||||
SwiftyBeaver.addDestination(file)
|
||||
}
|
||||
|
||||
CoreConfiguration.masksPrivateData = masksPrivateData
|
||||
}
|
||||
|
||||
public func doMigrations(_ profileManager: ProfileManager) {
|
||||
// profileManager.removeAllProfiles()
|
||||
guard didMigrateToV2 else {
|
||||
isDoingMigrations = true
|
||||
let migrated = doMigrateToV2()
|
||||
if !migrated.isEmpty {
|
||||
pp_log.info("Migrating \(migrated.count) profiles")
|
||||
migrated.forEach {
|
||||
var profile = $0
|
||||
if profileManager.isExistingProfile(withName: profile.header.name) {
|
||||
profile = profile.renamedUniquely(withLastUpdate: true)
|
||||
}
|
||||
profileManager.saveProfile(profile, isActive: nil)
|
||||
}
|
||||
} else {
|
||||
pp_log.info("Nothing to migrate!")
|
||||
}
|
||||
isDoingMigrations = false
|
||||
|
||||
didMigrateToV2 = true
|
||||
return
|
||||
}
|
||||
isDoingMigrations = false
|
||||
}
|
||||
|
||||
// MARK: Current state
|
||||
|
||||
public var preferences: AppPreferences {
|
||||
return DefaultAppPreferences(
|
||||
activeProfileId: activeProfileId,
|
||||
logFormat: logFormat,
|
||||
tunnelLogFormat: tunnelLogFormat,
|
||||
masksPrivateData: masksPrivateData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppManager: AppPreferences {
|
||||
public var activeProfileId: UUID? {
|
||||
get {
|
||||
guard let uuidString = defaults.string(forKey: DefaultKey.activeProfileId.rawValue) else {
|
||||
return nil
|
||||
}
|
||||
return UUID(uuidString: uuidString)
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue?.uuidString, forKey: DefaultKey.activeProfileId.rawValue)
|
||||
defaults.synchronize()
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
public var logFormat: String? {
|
||||
get {
|
||||
defaults.string(forKey: DefaultKey.logFormat.rawValue)
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue, forKey: DefaultKey.logFormat.rawValue)
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
public var tunnelLogFormat: String? {
|
||||
get {
|
||||
defaults.string(forKey: DefaultKey.tunnelLogFormat.rawValue)
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue, forKey: DefaultKey.tunnelLogFormat.rawValue)
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
public var masksPrivateData: Bool {
|
||||
get {
|
||||
defaults.bool(forKey: DefaultKey.masksPrivateData.rawValue)
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue, forKey: DefaultKey.masksPrivateData.rawValue)
|
||||
CoreConfiguration.masksPrivateData = newValue
|
||||
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internal use (readonly)
|
||||
|
||||
public private(set) var persistenceAuthor: String? {
|
||||
get {
|
||||
defaults.string(forKey: DefaultKey.persistenceAuthor.rawValue)
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue, forKey: DefaultKey.persistenceAuthor.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public internal(set) var didMigrateToV2: Bool {
|
||||
get {
|
||||
defaults.bool(forKey: DefaultKey.didMigrateToV2.rawValue)
|
||||
}
|
||||
set {
|
||||
defaults.set(newValue, forKey: DefaultKey.didMigrateToV2.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UserDefaults {
|
||||
func register(keyedDefaults: [AppManager.DefaultKey: Any?]) {
|
||||
let mapped = keyedDefaults.reduce(into: [String: Any]()) {
|
||||
$0[$1.key.rawValue] = $1.value
|
||||
}
|
||||
register(defaults: mapped)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// LogManager.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 6/15/22.
|
||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyBeaver
|
||||
|
||||
public class LogManager {
|
||||
public let logFile: URL?
|
||||
|
||||
public var logLevel: SwiftyBeaver.Level = .info
|
||||
|
||||
public var logFormat: String?
|
||||
|
||||
public init(logFile: URL?) {
|
||||
self.logFile = logFile
|
||||
}
|
||||
|
||||
public func configureLogging() {
|
||||
let console = ConsoleDestination()
|
||||
console.minLevel = logLevel
|
||||
// console.useNSLog = true
|
||||
if let logFormat = logFormat {
|
||||
console.format = logFormat
|
||||
}
|
||||
SwiftyBeaver.addDestination(console)
|
||||
|
||||
if let fileURL = logFile {
|
||||
let file = FileDestination()
|
||||
file.minLevel = logLevel
|
||||
file.logFileURL = fileURL
|
||||
if let logFormat = logFormat {
|
||||
file.format = logFormat
|
||||
}
|
||||
_ = file.deleteLogFile()
|
||||
SwiftyBeaver.addDestination(file)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,21 +25,50 @@
|
|||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import PassepartoutUtils
|
||||
|
||||
public class PersistenceManager {
|
||||
private let author: String?
|
||||
private let store: KeyValueStore
|
||||
|
||||
public init(author: String?) {
|
||||
self.author = author
|
||||
public init(store: KeyValueStore) {
|
||||
self.store = store
|
||||
|
||||
// set once
|
||||
if persistenceAuthor == nil {
|
||||
persistenceAuthor = UUID().uuidString
|
||||
}
|
||||
}
|
||||
|
||||
public func profilesPersistence(withName containerName: String) -> Persistence {
|
||||
let model = PassepartoutDataModels.profiles
|
||||
return Persistence(withCloudKitName: containerName, model: model, author: author)
|
||||
return Persistence(withCloudKitName: containerName, model: model, author: persistenceAuthor)
|
||||
}
|
||||
|
||||
public func providersPersistence(withName containerName: String) -> Persistence {
|
||||
let model = PassepartoutDataModels.providers
|
||||
return Persistence(withLocalName: containerName, model: model, author: author)
|
||||
return Persistence(withLocalName: containerName, model: model, author: persistenceAuthor)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: KeyValueStore
|
||||
|
||||
extension PersistenceManager {
|
||||
public private(set) var persistenceAuthor: String? {
|
||||
get {
|
||||
store.value(forLocation: StoreKey.persistenceAuthor)
|
||||
}
|
||||
set {
|
||||
store.setValue(newValue, forLocation: StoreKey.persistenceAuthor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PersistenceManager {
|
||||
private enum StoreKey: String, KeyStoreDomainLocation {
|
||||
case persistenceAuthor
|
||||
|
||||
var domain: String {
|
||||
"PersistenceManager"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// AppManager+Migrations.swift
|
||||
// UpgradeManager+Migrations.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 3/20/22.
|
||||
|
@ -28,12 +28,34 @@ import GenericJSON
|
|||
import TunnelKitCore
|
||||
import TunnelKitOpenVPNCore
|
||||
import TunnelKitManager
|
||||
import PassepartoutUtils
|
||||
|
||||
private typealias Map = [String: Any]
|
||||
|
||||
// MARK: Migrate old store
|
||||
|
||||
extension UpgradeManager {
|
||||
fileprivate enum LegacyStoreKey: String, KeyStoreLocation {
|
||||
case didMigrateToV2
|
||||
|
||||
var key: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
||||
func doMigrateStore(_ store: KeyValueStore) {
|
||||
if !didMigrateToV2 {
|
||||
guard let legacyDidMigrateToV2: Bool = store.value(forLocation: LegacyStoreKey.didMigrateToV2) else {
|
||||
return
|
||||
}
|
||||
didMigrateToV2 = legacyDidMigrateToV2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Migrate to version 2
|
||||
|
||||
extension AppManager {
|
||||
extension UpgradeManager {
|
||||
fileprivate enum MigrationError: Error {
|
||||
case json
|
||||
|
||||
|
@ -321,7 +343,7 @@ private extension URL {
|
|||
func asJSON() throws -> Map {
|
||||
let data = try Data(contentsOf: self)
|
||||
guard let json = try JSONSerialization.jsonObject(with: data) as? Map else {
|
||||
throw AppManager.MigrationError.json
|
||||
throw UpgradeManager.MigrationError.json
|
||||
}
|
||||
return json
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// UpgradeManager.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 2/8/22.
|
||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import SwiftyBeaver
|
||||
import PassepartoutUtils
|
||||
|
||||
@MainActor
|
||||
public class UpgradeManager: ObservableObject {
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
private let store: KeyValueStore
|
||||
|
||||
// MARK: State
|
||||
|
||||
@Published public private(set) var isDoingMigrations = true
|
||||
|
||||
public init(store: KeyValueStore) {
|
||||
self.store = store
|
||||
}
|
||||
|
||||
public func doMigrations(_ profileManager: ProfileManager) {
|
||||
doMigrateStore(store)
|
||||
|
||||
// profileManager.removeAllProfiles()
|
||||
guard didMigrateToV2 else {
|
||||
isDoingMigrations = true
|
||||
let migrated = doMigrateToV2()
|
||||
if !migrated.isEmpty {
|
||||
pp_log.info("Migrating \(migrated.count) profiles")
|
||||
migrated.forEach {
|
||||
var profile = $0
|
||||
if profileManager.isExistingProfile(withName: profile.header.name) {
|
||||
profile = profile.renamedUniquely(withLastUpdate: true)
|
||||
}
|
||||
profileManager.saveProfile(profile, isActive: nil)
|
||||
}
|
||||
} else {
|
||||
pp_log.info("Nothing to migrate!")
|
||||
}
|
||||
isDoingMigrations = false
|
||||
|
||||
didMigrateToV2 = true
|
||||
return
|
||||
}
|
||||
isDoingMigrations = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: KeyValueStore
|
||||
|
||||
extension UpgradeManager {
|
||||
public internal(set) var didMigrateToV2: Bool {
|
||||
get {
|
||||
store.value(forLocation: StoreKey.didMigrateToV2) ?? false
|
||||
}
|
||||
set {
|
||||
store.setValue(newValue, forLocation: StoreKey.didMigrateToV2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UpgradeManager {
|
||||
private enum StoreKey: String, KeyStoreDomainLocation {
|
||||
case didMigrateToV2
|
||||
|
||||
var domain: String {
|
||||
"UpgradeManager"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,13 @@
|
|||
import Foundation
|
||||
|
||||
extension VPNManager {
|
||||
private var vpnPreferences: VPNPreferences {
|
||||
DefaultVPNPreferences(
|
||||
tunnelLogFormat: tunnelLogFormat,
|
||||
masksPrivateData: masksPrivateData
|
||||
)
|
||||
}
|
||||
|
||||
func vpnConfigurationWithCurrentProfile() -> VPNConfiguration? {
|
||||
do {
|
||||
guard profileManager.isCurrentProfileActive() else {
|
||||
|
@ -52,7 +59,7 @@ extension VPNManager {
|
|||
let parameters = VPNConfigurationParameters(
|
||||
profile,
|
||||
appGroup: profileManager.appGroup,
|
||||
preferences: appManager.preferences,
|
||||
preferences: vpnPreferences,
|
||||
passwordReference: profileManager.passwordReference(forProfile: profile),
|
||||
withNetworkSettings: isNetworkSettingsSupported(),
|
||||
withCustomRules: isOnDemandRulesSupported()
|
||||
|
|
|
@ -25,8 +25,9 @@
|
|||
|
||||
import Foundation
|
||||
import Combine
|
||||
import TunnelKitCore
|
||||
import TunnelKitManager
|
||||
import TunnelKitOpenVPNManager
|
||||
import PassepartoutUtils
|
||||
|
||||
@MainActor
|
||||
public class VPNManager: ObservableObject {
|
||||
|
@ -34,7 +35,7 @@ public class VPNManager: ObservableObject {
|
|||
|
||||
// MARK: Initialization
|
||||
|
||||
let appManager: AppManager
|
||||
private let store: KeyValueStore
|
||||
|
||||
let profileManager: ProfileManager
|
||||
|
||||
|
@ -68,12 +69,12 @@ public class VPNManager: ObservableObject {
|
|||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
public init(
|
||||
appManager: AppManager,
|
||||
store: KeyValueStore,
|
||||
profileManager: ProfileManager,
|
||||
providerManager: ProviderManager,
|
||||
strategy: VPNManagerStrategy
|
||||
) {
|
||||
self.appManager = appManager
|
||||
self.store = store
|
||||
self.profileManager = profileManager
|
||||
self.providerManager = providerManager
|
||||
self.strategy = strategy
|
||||
|
@ -81,6 +82,8 @@ public class VPNManager: ObservableObject {
|
|||
isOnDemandRulesSupported = { true }
|
||||
|
||||
currentState = ObservableState()
|
||||
|
||||
CoreConfiguration.masksPrivateData = masksPrivateData
|
||||
}
|
||||
|
||||
public func toggle() -> Bool {
|
||||
|
@ -152,7 +155,7 @@ extension VPNManager {
|
|||
}
|
||||
|
||||
private func observeProfileManager() {
|
||||
profileManager.$activeProfileId
|
||||
profileManager.activeProfileIdPublisher
|
||||
.dropFirst()
|
||||
.removeDuplicates()
|
||||
.sink { newId in
|
||||
|
@ -252,3 +255,39 @@ extension VPNManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: KeyValueStore
|
||||
|
||||
extension VPNManager {
|
||||
public var tunnelLogFormat: String? {
|
||||
get {
|
||||
store.value(forLocation: StoreKey.tunnelLogFormat)
|
||||
}
|
||||
set {
|
||||
store.setValue(newValue, forLocation: StoreKey.tunnelLogFormat)
|
||||
}
|
||||
}
|
||||
|
||||
public var masksPrivateData: Bool {
|
||||
get {
|
||||
store.value(forLocation: StoreKey.masksPrivateData) ?? true
|
||||
}
|
||||
set {
|
||||
store.setValue(newValue, forLocation: StoreKey.masksPrivateData)
|
||||
|
||||
CoreConfiguration.masksPrivateData = masksPrivateData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VPNManager {
|
||||
private enum StoreKey: String, KeyStoreDomainLocation {
|
||||
case tunnelLogFormat
|
||||
|
||||
case masksPrivateData
|
||||
|
||||
var domain: String {
|
||||
"VPNManager"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ struct VPNConfigurationParameters {
|
|||
|
||||
let appGroup: String
|
||||
|
||||
let preferences: AppPreferences
|
||||
let preferences: VPNPreferences
|
||||
|
||||
let networkSettings: Profile.NetworkSettings
|
||||
|
||||
|
@ -54,7 +54,7 @@ struct VPNConfigurationParameters {
|
|||
init(
|
||||
_ profile: Profile,
|
||||
appGroup: String,
|
||||
preferences: AppPreferences,
|
||||
preferences: VPNPreferences,
|
||||
passwordReference: Data?,
|
||||
withNetworkSettings: Bool,
|
||||
withCustomRules: Bool
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
//
|
||||
// AppPreferences.swift
|
||||
// VPNPreferences.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 2/10/22.
|
||||
// Created by Davide De Rosa on 6/6/22.
|
||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
|
@ -25,23 +25,14 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
public protocol AppPreferences {
|
||||
var activeProfileId: UUID? { get }
|
||||
|
||||
var logFormat: String? { get }
|
||||
|
||||
public protocol VPNPreferences {
|
||||
var tunnelLogFormat: String? { get }
|
||||
|
||||
var masksPrivateData: Bool { get }
|
||||
}
|
||||
|
||||
public struct DefaultAppPreferences: AppPreferences {
|
||||
public let activeProfileId: UUID?
|
||||
struct DefaultVPNPreferences: VPNPreferences {
|
||||
let tunnelLogFormat: String?
|
||||
|
||||
public let logFormat: String?
|
||||
|
||||
public let tunnelLogFormat: String?
|
||||
|
||||
public let masksPrivateData: Bool
|
||||
let masksPrivateData: Bool
|
||||
}
|
|
@ -35,6 +35,8 @@ public class ProfileManager: ObservableObject {
|
|||
|
||||
// MARK: Initialization
|
||||
|
||||
private let store: KeyValueStore
|
||||
|
||||
private let providerManager: ProviderManager
|
||||
|
||||
public let appGroup: String
|
||||
|
@ -47,7 +49,7 @@ public class ProfileManager: ObservableObject {
|
|||
|
||||
// MARK: Observables
|
||||
|
||||
@Published public private(set) var activeProfileId: UUID? {
|
||||
@Published private var internalActiveProfileId: UUID? {
|
||||
willSet {
|
||||
pp_log.debug("Setting active profile: \(newValue?.uuidString ?? "nil")")
|
||||
}
|
||||
|
@ -59,6 +61,10 @@ public class ProfileManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public var activeProfileIdPublisher: Published<UUID?>.Publisher {
|
||||
$internalActiveProfileId
|
||||
}
|
||||
|
||||
public var currentProfileId: UUID? {
|
||||
get {
|
||||
internalCurrentProfileId
|
||||
|
@ -83,6 +89,7 @@ public class ProfileManager: ObservableObject {
|
|||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
public init(
|
||||
store: KeyValueStore,
|
||||
providerManager: ProviderManager,
|
||||
appGroup: String,
|
||||
keychainLabel: @escaping (String, VPNProtocolType) -> String,
|
||||
|
@ -91,6 +98,7 @@ public class ProfileManager: ObservableObject {
|
|||
guard let _ = UserDefaults(suiteName: appGroup) else {
|
||||
fatalError("No entitlements for group '\(appGroup)'")
|
||||
}
|
||||
self.store = store
|
||||
self.providerManager = providerManager
|
||||
self.appGroup = appGroup
|
||||
self.keychainLabel = keychainLabel
|
||||
|
@ -99,14 +107,6 @@ public class ProfileManager: ObservableObject {
|
|||
|
||||
currentProfile = ObservableProfile()
|
||||
}
|
||||
|
||||
public func setActiveProfileId(_ id: UUID) {
|
||||
guard isExistingProfile(withId: id) else {
|
||||
pp_log.warning("Active profile \(id) does not exist, ignoring")
|
||||
return
|
||||
}
|
||||
activeProfileId = id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Index
|
||||
|
@ -450,3 +450,41 @@ extension ProfileManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: KeyValueStore
|
||||
|
||||
extension ProfileManager {
|
||||
public private(set) var activeProfileId: UUID? {
|
||||
get {
|
||||
guard let idString: String = store.value(forLocation: StoreKey.activeProfileId) else {
|
||||
return nil
|
||||
}
|
||||
guard let id = UUID(uuidString: idString) else {
|
||||
pp_log.warning("Active profile id is malformed, ignoring")
|
||||
return nil
|
||||
}
|
||||
guard isExistingProfile(withId: id) else {
|
||||
pp_log.warning("Active profile \(id) does not exist, ignoring")
|
||||
return nil
|
||||
}
|
||||
return id
|
||||
}
|
||||
set {
|
||||
|
||||
// trigger publisher
|
||||
internalActiveProfileId = newValue
|
||||
|
||||
store.setValue(newValue?.uuidString, forLocation: StoreKey.activeProfileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProfileManager {
|
||||
private enum StoreKey: String, KeyStoreDomainLocation {
|
||||
case activeProfileId
|
||||
|
||||
var domain: String {
|
||||
"ProfileManager"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// KeyValueStore.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 6/15/22.
|
||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol KeyStoreLocation: RawRepresentable where RawValue == String {
|
||||
var key: String { get }
|
||||
}
|
||||
|
||||
public protocol KeyStoreDomainLocation: KeyStoreLocation {
|
||||
var domain: String { get }
|
||||
}
|
||||
|
||||
extension KeyStoreDomainLocation {
|
||||
public var key: String {
|
||||
"\(domain).\(rawValue)"
|
||||
}
|
||||
}
|
||||
|
||||
public protocol KeyValueStore {
|
||||
func setValue<L: KeyStoreLocation, V>(_ value: V, forLocation location: L)
|
||||
|
||||
func value<L: KeyStoreLocation, V>(forLocation location: L) -> V?
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// UserDefaultsStore.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 6/15/22.
|
||||
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct UserDefaultsStore: KeyValueStore {
|
||||
private let defaults: UserDefaults
|
||||
|
||||
public init(defaults: UserDefaults) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
public func setValue<L: KeyStoreLocation, V>(_ value: V, forLocation location: L) {
|
||||
defaults.setValue(value, forKey: location.key)
|
||||
}
|
||||
|
||||
public func value<L: KeyStoreLocation, V>(forLocation location: L) -> V? {
|
||||
defaults.value(forKey: location.key) as? V
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue