263 lines
7.4 KiB
Swift
263 lines
7.4 KiB
Swift
//
|
|
// ProfileEditor.swift
|
|
// Passepartout
|
|
//
|
|
// Created by Davide De Rosa on 2/17/24.
|
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
//
|
|
// https://github.com/passepartoutvpn
|
|
//
|
|
// This file is part of Passepartout.
|
|
//
|
|
// Passepartout is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Passepartout is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
import Combine
|
|
import CommonLibrary
|
|
import Foundation
|
|
import PassepartoutKit
|
|
|
|
@MainActor
|
|
public final class ProfileEditor: ObservableObject {
|
|
|
|
@Published
|
|
private var editableProfile: EditableProfile
|
|
|
|
@Published
|
|
public var isShared: Bool
|
|
|
|
@Published
|
|
private var trackedPreferences: [UUID: ModulePreferencesRepository]
|
|
|
|
private(set) var removedModules: [UUID: any ModuleBuilder]
|
|
|
|
public convenience init() {
|
|
self.init(modules: [])
|
|
}
|
|
|
|
// for testing/previews
|
|
public init(profile: Profile) {
|
|
editableProfile = profile.editable()
|
|
isShared = false
|
|
trackedPreferences = [:]
|
|
removedModules = [:]
|
|
}
|
|
|
|
// for testing/previews
|
|
public init(modules: [any ModuleBuilder]) {
|
|
editableProfile = EditableProfile(
|
|
modules: modules,
|
|
activeModulesIds: Set(modules.map(\.id))
|
|
)
|
|
isShared = false
|
|
trackedPreferences = [:]
|
|
removedModules = [:]
|
|
}
|
|
}
|
|
|
|
// MARK: - Types
|
|
|
|
extension ProfileEditor {
|
|
public var moduleTypes: [ModuleType] {
|
|
editableProfile.modules
|
|
.map(\.moduleType)
|
|
}
|
|
|
|
public var availableModuleTypes: [ModuleType] {
|
|
ModuleType
|
|
.allCases
|
|
.filter {
|
|
// TODO: #657, hide manual OpenVPN/WireGuard until editable
|
|
// $0 != .openVPN && $0 != .wireGuard
|
|
$0 != .wireGuard
|
|
}
|
|
.filter {
|
|
!moduleTypes.contains($0)
|
|
}
|
|
.sorted {
|
|
$0.localizedDescription.lowercased() < $1.localizedDescription.lowercased()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Editing
|
|
|
|
extension ProfileEditor {
|
|
public var profile: EditableProfile {
|
|
get {
|
|
editableProfile
|
|
}
|
|
set {
|
|
editableProfile = newValue
|
|
}
|
|
}
|
|
|
|
public var isAvailableForTV: Bool {
|
|
get {
|
|
editableProfile.attributes.isAvailableForTV == true
|
|
}
|
|
set {
|
|
editableProfile.attributes.isAvailableForTV = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ProfileEditor {
|
|
public var modules: [any ModuleBuilder] {
|
|
editableProfile.modules
|
|
}
|
|
|
|
public var activeModules: [any ModuleBuilder] {
|
|
editableProfile.modules.filter {
|
|
isActiveModule(withId: $0.id)
|
|
}
|
|
}
|
|
|
|
public func module(withId moduleId: UUID) -> (any ModuleBuilder)? {
|
|
editableProfile.modules.first {
|
|
$0.id == moduleId
|
|
} ?? removedModules[moduleId]
|
|
}
|
|
|
|
public func isActiveModule(withId moduleId: UUID) -> Bool {
|
|
editableProfile.isActiveModule(withId: moduleId)
|
|
}
|
|
|
|
public func toggleModule(withId moduleId: UUID) {
|
|
guard let existingModule = module(withId: moduleId) else {
|
|
return
|
|
}
|
|
if isActiveModule(withId: moduleId) {
|
|
editableProfile.activeModulesIds.remove(moduleId)
|
|
} else {
|
|
activateModule(existingModule)
|
|
}
|
|
}
|
|
|
|
public func moveModules(from offsets: IndexSet, to newOffset: Int) {
|
|
editableProfile.modules.move(fromOffsets: offsets, toOffset: newOffset)
|
|
}
|
|
|
|
public func removeModules(at offsets: IndexSet) {
|
|
offsets.forEach {
|
|
let module = editableProfile.modules[$0]
|
|
removedModules[module.id] = module
|
|
editableProfile.modules.remove(at: $0)
|
|
}
|
|
}
|
|
|
|
public func removeModule(withId moduleId: UUID) {
|
|
guard let index = editableProfile.modules.firstIndex(where: { $0.id == moduleId }) else {
|
|
return
|
|
}
|
|
let module = editableProfile.modules[index]
|
|
removedModules[module.id] = module
|
|
editableProfile.modules.remove(at: index)
|
|
}
|
|
|
|
public func saveModule(_ module: any ModuleBuilder, activating: Bool) {
|
|
if let index = editableProfile.modules.firstIndex(where: { $0.id == module.id }) {
|
|
editableProfile.modules[index] = module
|
|
} else {
|
|
editableProfile.modules.append(module)
|
|
}
|
|
if activating {
|
|
activateModule(module)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension ProfileEditor {
|
|
func activateModule(_ module: any ModuleBuilder) {
|
|
editableProfile.activeModulesIds.insert(module.id)
|
|
}
|
|
}
|
|
|
|
// MARK: - Building
|
|
|
|
extension ProfileEditor {
|
|
public func build() throws -> Profile {
|
|
let builder = try editableProfile.builder()
|
|
let profile = try builder.tryBuild()
|
|
|
|
// update local view
|
|
editableProfile.modules = profile.modulesBuilders()
|
|
removedModules.removeAll()
|
|
|
|
return profile
|
|
}
|
|
}
|
|
|
|
// MARK: - Load/Save
|
|
|
|
extension ProfileEditor {
|
|
public func load(_ profile: EditableProfile, isShared: Bool) {
|
|
editableProfile = profile
|
|
self.isShared = isShared
|
|
removedModules = [:]
|
|
}
|
|
|
|
public func loadPreferences(
|
|
_ preferences: ModulePreferences,
|
|
from manager: PreferencesManager,
|
|
forModuleWithId moduleId: UUID
|
|
) {
|
|
do {
|
|
pp_log(.App.profiles, .debug, "Track preferences for module \(moduleId)")
|
|
let repository = try trackedPreferences[moduleId] ?? manager.preferencesRepository(forModuleWithId: moduleId)
|
|
preferences.repository = repository
|
|
trackedPreferences[moduleId] = repository // @Published
|
|
} catch {
|
|
pp_log(.App.profiles, .error, "Unable to track preferences for module \(moduleId): \(error)")
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
public func save(to profileManager: ProfileManager) async throws -> Profile {
|
|
do {
|
|
let newProfile = try build()
|
|
try await profileManager.save(newProfile, isLocal: true, remotelyShared: isShared)
|
|
trackedPreferences.forEach {
|
|
do {
|
|
pp_log(.App.profiles, .debug, "Save tracked preferences for module \($0.key)")
|
|
try $0.value.save()
|
|
} catch {
|
|
pp_log(.App.profiles, .error, "Unable to save preferences for profile \(profile.id): \(error)")
|
|
}
|
|
}
|
|
trackedPreferences.removeAll()
|
|
return newProfile
|
|
} catch {
|
|
pp_log(.App.profiles, .fault, "Unable to save edited profile: \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
public func discard() {
|
|
trackedPreferences.forEach {
|
|
pp_log(.App.profiles, .debug, "Discard tracked preferences for module \($0.key)")
|
|
$0.value.discard()
|
|
}
|
|
trackedPreferences.removeAll()
|
|
}
|
|
}
|
|
|
|
// MARK: - Testing
|
|
|
|
extension ProfileEditor {
|
|
var activeModulesIds: Set<UUID> {
|
|
editableProfile.activeModulesIds
|
|
}
|
|
}
|