diff --git a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 34bf07a7..a1b1332b 100644 --- a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,7 +32,7 @@ "kind" : "remoteSourceControl", "location" : "git@github.com:passepartoutvpn/passepartoutkit-source", "state" : { - "revision" : "779910e268e79f1004a95285ac2485255d88bb21" + "revision" : "f681f968f39ca514e29ac6c0abcf658c224e4c04" } }, { diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme index 0a61ae41..68dce74f 100644 --- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme +++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme @@ -59,6 +59,14 @@ argument = "-com.apple.CoreData.SQLDebug 1" isEnabled = "NO"> + + + + diff --git a/Passepartout/Library/Package.swift b/Passepartout/Library/Package.swift index 6618f1f6..eab5aed9 100644 --- a/Passepartout/Library/Package.swift +++ b/Passepartout/Library/Package.swift @@ -31,7 +31,7 @@ let package = Package( ], dependencies: [ // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.8.0"), - .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "779910e268e79f1004a95285ac2485255d88bb21"), + .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "f681f968f39ca514e29ac6c0abcf658c224e4c04"), // .package(path: "../../../passepartoutkit-source"), .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.8.0"), // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"), diff --git a/Passepartout/Library/Sources/AppLibrary/Business/ProfileManager.swift b/Passepartout/Library/Sources/AppLibrary/Business/ProfileManager.swift index b798a014..7083208d 100644 --- a/Passepartout/Library/Sources/AppLibrary/Business/ProfileManager.swift +++ b/Passepartout/Library/Sources/AppLibrary/Business/ProfileManager.swift @@ -50,7 +50,7 @@ public final class ProfileManager: ObservableObject { private var allProfiles: [Profile.ID: Profile] { didSet { - reloadFilteredProfiles() + reloadFilteredProfiles(with: searchSubject.value) } } @@ -117,16 +117,36 @@ extension ProfileManager { } } - public func save(_ profile: Profile) async throws { + public func save(_ profile: Profile, isShared: Bool? = nil) async throws { + pp_log(.app, .notice, "Save profile \(profile.id)...") do { - try await beforeSave?(profile) - try await repository.saveEntities([profile]) - allProfiles[profile.id] = profile - didChange.send(.save(profile)) + if let existingProfile = allProfiles[profile.id], profile != existingProfile { + try await beforeSave?(profile) + try await repository.saveEntities([profile]) + + allProfiles[profile.id] = profile + didChange.send(.save(profile)) + } else { + pp_log(.app, .notice, "Profile \(profile.id) not modified, not saving") + } } catch { pp_log(.app, .fault, "Unable to save profile \(profile.id): \(error)") throw error } + do { + if let isShared, let remoteRepository { + if isShared { + pp_log(.app, .notice, "Enable remote sharing of profile \(profile.id)...") + try await remoteRepository.saveEntities([profile]) + } else { + pp_log(.app, .notice, "Disable remote sharing of profile \(profile.id)...") + try await remoteRepository.removeEntities(withIds: [profile.id]) + } + } + } catch { + pp_log(.app, .fault, "Unable to save/remove remote profile \(profile.id): \(error)") + throw error + } } public func remove(withId profileId: Profile.ID) async { @@ -134,14 +154,15 @@ extension ProfileManager { } public func remove(withIds profileIds: [Profile.ID]) async { + pp_log(.app, .notice, "Remove profiles \(profileIds)...") do { // remove local profiles var newAllProfiles = allProfiles try await repository.removeEntities(withIds: profileIds) + await afterRemove?(profileIds) profileIds.forEach { newAllProfiles.removeValue(forKey: $0) } - await afterRemove?(profileIds) // remove remote counterpart too try? await remoteRepository?.removeEntities(withIds: profileIds) @@ -169,22 +190,8 @@ extension ProfileManager { allRemoteProfiles.keys.contains(profileId) } - public func setRemotelyShared(_ shared: Bool, profileWithId profileId: Profile.ID) async throws { - guard let remoteRepository else { - pp_log(.app, .error, "Unable to share remotely when no remoteRepository is set") - return - } - guard let profile = allProfiles[profileId] else { - return - } - if shared { - try await remoteRepository.saveEntities([profile]) - } else { - try await remoteRepository.removeEntities(withIds: [profileId]) - } - } - - public func eraseRemoteProfiles() async throws { + public func eraseRemotelySharedProfiles() async throws { + pp_log(.app, .notice, "Erase remotely shared profiles...") try await remoteRepository?.removeEntities(withIds: Array(allRemoteProfiles.keys)) } } @@ -209,6 +216,7 @@ extension ProfileManager { var builder = profile.builder(withNewId: true) builder.name = firstUniqueName(from: profile.name) + pp_log(.app, .notice, "Duplicate profile [\(profileId), \(profile.name)] -> [\(builder.id), \(builder.name)]...") let copy = try builder.tryBuild() try await save(copy) @@ -239,7 +247,7 @@ extension ProfileManager { .first() .receive(on: DispatchQueue.main) .sink { [weak self] in - self?.notifyLocalEntities($0) + self?.reloadLocalProfiles($0) } .store(in: &subscriptions) @@ -247,7 +255,16 @@ extension ProfileManager { .entitiesPublisher .receive(on: DispatchQueue.main) .sink { [weak self] in - self?.notifyRemoteEntities($0) + self?.reloadRemoteProfiles($0) + } + .store(in: &subscriptions) + + remoteRepository? + .entitiesPublisher + .dropFirst() + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.importRemoteProfiles($0) } .store(in: &subscriptions) @@ -261,26 +278,26 @@ extension ProfileManager { } private extension ProfileManager { - func notifyLocalEntities(_ result: EntitiesResult) { + func reloadLocalProfiles(_ result: EntitiesResult) { + pp_log(.app, .info, "Reload local profiles: \(result.entities.map(\.id))") allProfiles = result.entities.reduce(into: [:]) { $0[$1.id] = $1 } } - func notifyRemoteEntities(_ result: EntitiesResult) { - let isInitial = allRemoteProfiles.isEmpty + func reloadRemoteProfiles(_ result: EntitiesResult) { + pp_log(.app, .info, "Reload remote profiles: \(result.entities.map(\.id))") allRemoteProfiles = result.entities.reduce(into: [:]) { $0[$1.id] = $1 } objectWillChange.send() + } - // do not import on initial load - guard !isInitial else { - return - } + // pull remote updates into local profiles (best-effort) + func importRemoteProfiles(_ result: EntitiesResult) { + let profilesToImport = result.entities + pp_log(.app, .info, "Try to import remote profiles: \(result.entities.map(\.id))") - // pull remote updates into local profiles (best-effort) - let profilesToImport = allRemoteProfiles.values Task.detached { [weak self] in for remoteProfile in profilesToImport { do { @@ -294,14 +311,15 @@ private extension ProfileManager { } func performSearch(_ search: String) { + pp_log(.app, .notice, "Filter profiles with '\(search)'") reloadFilteredProfiles(with: search) } - func reloadFilteredProfiles(with search: String? = nil) { + func reloadFilteredProfiles(with search: String) { profiles = allProfiles .values .filter { - if let search, !search.isEmpty { + if !search.isEmpty { return $0.name.lowercased().contains(search.lowercased()) } return true diff --git a/Passepartout/Library/Sources/AppUI/Business/ProfileEditor.swift b/Passepartout/Library/Sources/AppUI/Business/ProfileEditor.swift index a7cf5822..5d669fbc 100644 --- a/Passepartout/Library/Sources/AppUI/Business/ProfileEditor.swift +++ b/Passepartout/Library/Sources/AppUI/Business/ProfileEditor.swift @@ -272,8 +272,7 @@ extension ProfileEditor { func save(to profileManager: ProfileManager) async throws { do { let newProfile = try build() - try await profileManager.save(newProfile) - try await profileManager.setRemotelyShared(isShared, profileWithId: newProfile.id) + try await profileManager.save(newProfile, isShared: isShared) } catch { pp_log(.app, .fault, "Unable to save edited profile: \(error)") throw error diff --git a/Passepartout/Library/Sources/AppUI/Views/Settings/SettingsSectionGroup.swift b/Passepartout/Library/Sources/AppUI/Views/Settings/SettingsSectionGroup.swift index a102b201..776912fc 100644 --- a/Passepartout/Library/Sources/AppUI/Views/Settings/SettingsSectionGroup.swift +++ b/Passepartout/Library/Sources/AppUI/Views/Settings/SettingsSectionGroup.swift @@ -86,7 +86,7 @@ private extension SettingsSectionGroup { Task { do { pp_log(.app, .info, "Erase CloudKit profiles...") - try await profileManager.eraseRemoteProfiles() + try await profileManager.eraseRemotelySharedProfiles() let containerId = BundleConfiguration.mainString(for: .cloudKitId) pp_log(.app, .info, "Erase CloudKit store with identifier \(containerId)...") diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 54b185ba..efcf93b5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,12 +21,6 @@ logname = "CHANGELOG.txt" desc "Bump version" lane :bump do |options| - version = options[:version] - build = options[:build] - increment_build_number(build_number: build) - unless version.nil? || version.empty? - increment_version_number_in_xcodeproj(version_number: version) - end unless options[:only] log = changelog_from_git_commits( pretty: "* %h %s", @@ -36,7 +30,13 @@ lane :bump do |options| File.open(path, "w") { |file| file.write(log) } - system("vim", path) + system("vim", path) or UI.user_error!("CHANGELOG editor cancelled") + end + version = options[:version] + build = options[:build] + increment_build_number(build_number: build) + unless version.nil? || version.empty? + increment_version_number_in_xcodeproj(version_number: version) end commit_version_bump( message: "Bump version",