Replace active modules count with a description (#915)

To get access to modules, try to avoid full Profile objects. Instead,
replace the coupled ProfileHeader occurrences with a new intermediary
ProfilePreview everywhere.

This way, a ProfileProcessor can inject the localized modules
descriptions from above with the preview() method.
This commit is contained in:
Davide 2024-11-22 12:52:51 +01:00 committed by GitHub
parent a2f246454d
commit 4b4fca8344
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 229 additions and 145 deletions

View File

@ -124,7 +124,7 @@ private extension InstalledProfileView {
ProfileContextMenu(
profileManager: profileManager,
tunnel: tunnel,
header: (profile ?? .mock).header(),
preview: .init(profile ?? .mock),
interactiveManager: interactiveManager,
errorHandler: errorHandler,
isInstalledProfile: true,
@ -324,7 +324,7 @@ private struct ContentView: View {
style: .full,
profileManager: .mock,
tunnel: .mock,
header: Profile.mock.header(),
preview: .init(.mock),
interactiveManager: InteractiveManager(),
errorHandler: .default(),
nextProfileId: .constant(nil),

View File

@ -24,7 +24,6 @@
//
import CommonLibrary
import PassepartoutKit
import SwiftUI
struct ProfileCardView: View {
@ -36,21 +35,22 @@ struct ProfileCardView: View {
let style: Style
let header: ProfileHeader
let preview: ProfilePreview
var body: some View {
switch style {
case .compact:
Text(header.name)
Text(preview.name)
.themeTruncating()
.frame(maxWidth: .infinity, alignment: .leading)
case .full:
VStack(alignment: .leading) {
Text(header.name)
Text(preview.name)
.font(.headline)
.themeTruncating()
Text(Strings.Views.Profiles.Rows.modules(header.modules.count))
Text(preview.subtitle ?? Strings.Views.Profiles.Rows.noModules)
.font(.subheadline)
.foregroundStyle(.secondary)
}
@ -66,13 +66,13 @@ struct ProfileCardView: View {
Section {
ProfileCardView(
style: .compact,
header: Profile.mock.header()
preview: .init(.mock)
)
}
Section {
ProfileCardView(
style: .full,
header: Profile.mock.header()
preview: .init(.mock)
)
}
}

View File

@ -136,7 +136,7 @@ private struct ContainerModifier: ViewModifier {
profileManager.search(byName: $0)
}
.themeAnimation(on: profileManager.isReady, category: .profiles)
.themeAnimation(on: profileManager.headers, category: .profiles)
.themeAnimation(on: profileManager.previews, category: .profiles)
}
}

View File

@ -33,7 +33,7 @@ struct ProfileContextMenu: View, Routable {
let tunnel: ExtendedTunnel
let header: ProfileHeader
let preview: ProfilePreview
let interactiveManager: InteractiveManager
@ -60,7 +60,7 @@ struct ProfileContextMenu: View, Routable {
@MainActor
private extension ProfileContextMenu {
var profile: Profile? {
profileManager.profile(withId: header.id)
profileManager.profile(withId: preview.id)
}
var tunnelToggleButton: some View {
@ -111,7 +111,7 @@ private extension ProfileContextMenu {
var profileEditButton: some View {
Button {
flow?.onEditProfile(header)
flow?.onEditProfile(preview)
} label: {
ThemeImageLabel(Strings.Global.edit.withTrailingDots, .profileEdit)
}
@ -120,7 +120,7 @@ private extension ProfileContextMenu {
var profileDuplicateButton: some View {
ProfileDuplicateButton(
profileManager: profileManager,
header: header,
preview: preview,
errorHandler: errorHandler
) {
ThemeImageLabel(Strings.Global.duplicate, .contextDuplicate)
@ -130,7 +130,7 @@ private extension ProfileContextMenu {
var profileRemoveButton: some View {
ProfileRemoveButton(
profileManager: profileManager,
header: header
preview: preview
) {
ThemeImageLabel(Strings.Global.remove, .contextRemove)
}
@ -143,7 +143,7 @@ private extension ProfileContextMenu {
ProfileContextMenu(
profileManager: .mock,
tunnel: .mock,
header: Profile.mock.header(),
preview: .init(.mock),
interactiveManager: InteractiveManager(),
errorHandler: .default(),
isInstalledProfile: true

View File

@ -25,13 +25,12 @@
import CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
struct ProfileDuplicateButton<Label>: View where Label: View {
let profileManager: ProfileManager
let header: ProfileHeader
let preview: ProfilePreview
let errorHandler: ErrorHandler
@ -41,12 +40,12 @@ struct ProfileDuplicateButton<Label>: View where Label: View {
Button {
Task {
do {
try await profileManager.duplicate(profileWithId: header.id)
try await profileManager.duplicate(profileWithId: preview.id)
} catch {
errorHandler.handle(
error,
title: Strings.Global.duplicate,
message: Strings.Views.Profiles.Errors.duplicate(header.name)
message: Strings.Views.Profiles.Errors.duplicate(preview.name)
)
}
}

View File

@ -28,7 +28,7 @@ import Foundation
import PassepartoutKit
struct ProfileFlow {
let onEditProfile: (ProfileHeader) -> Void
let onEditProfile: (ProfilePreview) -> Void
let onEditProviderEntity: (Profile) -> Void

View File

@ -64,7 +64,7 @@ struct ProfileGridView: View, Routable, TunnelInstallationProviding {
.unanimated()
}
LazyVGrid(columns: columns) {
ForEach(allHeaders, content: profileView)
ForEach(allPreviews, content: profileView)
.onDelete { offsets in
Task {
await profileManager.removeProfiles(at: offsets)
@ -88,8 +88,8 @@ struct ProfileGridView: View, Routable, TunnelInstallationProviding {
// MARK: - Subviews
private extension ProfileGridView {
var allHeaders: [ProfileHeader] {
profileManager.headers
var allPreviews: [ProfilePreview] {
profileManager.previews
}
func headerView(scrollProxy: ScrollViewProxy) -> some View {
@ -108,7 +108,7 @@ private extension ProfileGridView {
ProfileContextMenu(
profileManager: profileManager,
tunnel: tunnel,
header: $0.header(),
preview: .init($0),
interactiveManager: interactiveManager,
errorHandler: errorHandler,
isInstalledProfile: true,
@ -118,31 +118,31 @@ private extension ProfileGridView {
}
}
func profileView(for header: ProfileHeader) -> some View {
func profileView(for preview: ProfilePreview) -> some View {
ProfileRowView(
style: .full,
profileManager: profileManager,
tunnel: tunnel,
header: header,
preview: preview,
interactiveManager: interactiveManager,
errorHandler: errorHandler,
nextProfileId: $nextProfileId,
withMarker: true,
flow: flow
)
.themeGridCell(isSelected: header.id == nextProfileId ?? currentProfile?.id)
.themeGridCell(isSelected: preview.id == nextProfileId ?? currentProfile?.id)
.contextMenu {
ProfileContextMenu(
profileManager: profileManager,
tunnel: tunnel,
header: header,
preview: preview,
interactiveManager: interactiveManager,
errorHandler: errorHandler,
isInstalledProfile: false,
flow: flow
)
}
.id(header.id)
.id(preview.id)
}
}

View File

@ -23,17 +23,17 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import PassepartoutKit
import CommonLibrary
import SwiftUI
struct ProfileInfoButton: View {
let header: ProfileHeader
let preview: ProfilePreview
let onEdit: (ProfileHeader) -> Void
let onEdit: (ProfilePreview) -> Void
var body: some View {
Button {
onEdit(header)
onEdit(preview)
} label: {
ThemeImage(.info)
}

View File

@ -63,7 +63,7 @@ struct ProfileListView: View, Routable, TunnelInstallationProviding {
.unanimated()
}
Group {
ForEach(allHeaders, content: profileView)
ForEach(allPreviews, content: profileView)
.onDelete { offsets in
Task {
await profileManager.removeProfiles(at: offsets)
@ -78,8 +78,8 @@ struct ProfileListView: View, Routable, TunnelInstallationProviding {
}
private extension ProfileListView {
var allHeaders: [ProfileHeader] {
profileManager.headers
var allPreviews: [ProfilePreview] {
profileManager.previews
}
func headerView(scrollProxy: ScrollViewProxy) -> some View {
@ -98,7 +98,7 @@ private extension ProfileListView {
ProfileContextMenu(
profileManager: profileManager,
tunnel: tunnel,
header: $0.header(),
preview: .init($0),
interactiveManager: interactiveManager,
errorHandler: errorHandler,
isInstalledProfile: true,
@ -108,12 +108,12 @@ private extension ProfileListView {
}
}
func profileView(for header: ProfileHeader) -> some View {
func profileView(for preview: ProfilePreview) -> some View {
ProfileRowView(
style: cardStyle,
profileManager: profileManager,
tunnel: tunnel,
header: header,
preview: preview,
interactiveManager: interactiveManager,
errorHandler: errorHandler,
nextProfileId: $nextProfileId,
@ -124,14 +124,14 @@ private extension ProfileListView {
ProfileContextMenu(
profileManager: profileManager,
tunnel: tunnel,
header: header,
preview: preview,
interactiveManager: interactiveManager,
errorHandler: errorHandler,
isInstalledProfile: false,
flow: flow
)
}
.id(header.id)
.id(preview.id)
}
}

View File

@ -24,20 +24,19 @@
//
import CommonLibrary
import PassepartoutKit
import SwiftUI
struct ProfileRemoveButton<Label>: View where Label: View {
let profileManager: ProfileManager
let header: ProfileHeader
let preview: ProfilePreview
let label: () -> Label
var body: some View {
Button(role: .destructive) {
Task {
await profileManager.remove(withId: header.id)
await profileManager.remove(withId: preview.id)
}
} label: {
label()

View File

@ -40,7 +40,7 @@ struct ProfileRowView: View, Routable {
let tunnel: ExtendedTunnel
let header: ProfileHeader
let preview: ProfilePreview
let interactiveManager: InteractiveManager
@ -67,7 +67,7 @@ struct ProfileRowView: View, Routable {
attributes: attributes,
isRemoteImportingEnabled: profileManager.isRemoteImportingEnabled
)
ProfileInfoButton(header: header) {
ProfileInfoButton(preview: preview) {
flow?.onEditProfile($0)
}
}
@ -79,7 +79,7 @@ struct ProfileRowView: View, Routable {
// MARK: - Subviews (observing)
private struct MarkerView: View {
let headerId: Profile.ID
let profileId: Profile.ID
let nextProfileId: Profile.ID?
@ -90,8 +90,8 @@ private struct MarkerView: View {
var body: some View {
ZStack {
ThemeImage(headerId == nextProfileId ? .pending : tunnel.statusImageName)
.opaque(requiredFeatures == nil && (headerId == nextProfileId || headerId == tunnel.currentProfile?.id))
ThemeImage(profileId == nextProfileId ? .pending : tunnel.statusImageName)
.opaque(requiredFeatures == nil && (profileId == nextProfileId || profileId == tunnel.currentProfile?.id))
if let requiredFeatures {
PurchaseRequiredButton(features: requiredFeatures, paywallReason: .constant(nil))
@ -102,9 +102,13 @@ private struct MarkerView: View {
}
private extension ProfileRowView {
var profile: Profile? {
profileManager.profile(withId: preview.id)
}
var markerView: some View {
MarkerView(
headerId: header.id,
profileId: preview.id,
nextProfileId: nextProfileId,
tunnel: tunnel,
requiredFeatures: requiredFeatures
@ -114,7 +118,7 @@ private extension ProfileRowView {
var cardView: some View {
TunnelToggleButton(
tunnel: tunnel,
profile: profileManager.profile(withId: header.id),
profile: profile,
nextProfileId: $nextProfileId,
interactiveManager: interactiveManager,
errorHandler: errorHandler,
@ -127,7 +131,7 @@ private extension ProfileRowView {
label: { _ in
ProfileCardView(
style: style,
header: header
preview: preview
)
.frame(maxWidth: .infinity)
.contentShape(.rect)
@ -146,15 +150,15 @@ private extension ProfileRowView {
}
var requiredFeatures: Set<AppFeature>? {
profileManager.requiredFeatures(forProfileWithId: header.id)
profileManager.requiredFeatures(forProfileWithId: preview.id)
}
var isShared: Bool {
profileManager.isRemotelyShared(profileWithId: header.id)
profileManager.isRemotelyShared(profileWithId: preview.id)
}
var isTV: Bool {
isShared && profileManager.isAvailableForTV(profileWithId: header.id)
isShared && profileManager.isAvailableForTV(profileWithId: preview.id)
}
}
@ -169,7 +173,7 @@ private extension ProfileRowView {
style: .compact,
profileManager: profileManager,
tunnel: .mock,
header: profile.header(),
preview: .init(profile),
interactiveManager: InteractiveManager(),
errorHandler: .default(),
nextProfileId: .constant(nil),

View File

@ -80,19 +80,19 @@ private extension AppMenu {
}
var profilesList: some View {
ForEach(profileManager.headers, id: \.id, content: profileToggle)
ForEach(profileManager.previews, id: \.id, content: profileToggle)
}
func profileToggle(for header: ProfileHeader) -> some View {
Toggle(header.name, isOn: profileToggleBinding(for: header))
func profileToggle(for preview: ProfilePreview) -> some View {
Toggle(preview.name, isOn: profileToggleBinding(for: preview))
}
func profileToggleBinding(for header: ProfileHeader) -> Binding<Bool> {
func profileToggleBinding(for preview: ProfilePreview) -> Binding<Bool> {
Binding {
header.id == tunnel.currentProfile?.id && tunnel.status != .inactive
preview.id == tunnel.currentProfile?.id && tunnel.status != .inactive
} set: { isOn in
Task {
guard let profile = profileManager.profile(withId: header.id) else {
guard let profile = profileManager.profile(withId: preview.id) else {
return
}
do {
@ -102,7 +102,7 @@ private extension AppMenu {
try await tunnel.disconnect()
}
} catch {
pp_log(.app, .error, "Unable to toggle profile \(header.id) from menu: \(error)")
pp_log(.app, .error, "Unable to toggle profile \(preview.id) from menu: \(error)")
}
}
}

View File

@ -145,7 +145,7 @@ private extension MigrateView {
model.step = .fetching
pp_log(.App.migration, .notice, "Fetch migratable profiles...")
let migratable = try await migrationManager.fetchMigratableProfiles()
let knownIDs = Set(profileManager.headers.map(\.id))
let knownIDs = Set(profileManager.previews.map(\.id))
model.profiles = migratable.filter {
!knownIDs.contains($0.id)
}

View File

@ -99,9 +99,9 @@ private extension ActiveProfileView {
func detailView(for profile: Profile) -> some View {
VStack(spacing: 10) {
if let connectionModule {
if let connectionType = profile.localizedDescription(optionalStyle: .connectionType) {
DetailRowView(title: Strings.Global.protocol) {
Text(connectionModule.moduleHandler.id.name)
Text(connectionType)
}
}
if let pair = profile.selectedProvider {
@ -116,8 +116,8 @@ private extension ActiveProfileView {
}
}
}
if let otherModulesList {
DetailRowView(title: otherModulesList) {
if let otherList = profile.localizedDescription(optionalStyle: .nonConnectionTypes) {
DetailRowView(title: otherList) {
EmptyView()
}
}
@ -167,28 +167,6 @@ private extension ActiveProfileView {
}
}
private extension ActiveProfileView {
var connectionModule: ConnectionModule? {
profile?.firstConnectionModule(ifActive: true)
}
var otherModules: [Module]? {
profile?
.activeModules
.filter {
!($0 is ConnectionModule)
}
.nilIfEmpty
}
var otherModulesList: String? {
otherModules?
.map(\.moduleType.localizedDescription)
.sorted()
.joined(separator: ", ")
}
}
// MARK: -
private extension ActiveProfileView {

View File

@ -50,7 +50,7 @@ struct ProfileListView: View {
headerView
.frame(maxWidth: .infinity, alignment: .leading)
List {
ForEach(headers, id: \.id, content: toggleButton(for:))
ForEach(previews, id: \.id, content: toggleButton(for:))
}
.listStyle(.grouped)
.scrollClipDisabled()
@ -63,8 +63,8 @@ struct ProfileListView: View {
}
private extension ProfileListView {
var headers: [ProfileHeader] {
profileManager.headers
var previews: [ProfilePreview] {
profileManager.previews
}
var headerView: some View {
@ -74,28 +74,28 @@ private extension ProfileListView {
.font(.body)
}
func toggleButton(for header: ProfileHeader) -> some View {
func toggleButton(for preview: ProfilePreview) -> some View {
TunnelToggleButton(
tunnel: tunnel,
profile: profileManager.profile(withId: header.id),
profile: profileManager.profile(withId: preview.id),
nextProfileId: .constant(nil),
interactiveManager: interactiveManager,
errorHandler: errorHandler,
onProviderEntityRequired: onProviderEntityRequired,
onPurchaseRequired: onPurchaseRequired,
label: { _ in
toggleView(for: header)
toggleView(for: preview)
}
)
.focused($focusedField, equals: .profile(header.id))
.focused($focusedField, equals: .profile(preview.id))
}
func toggleView(for header: ProfileHeader) -> some View {
func toggleView(for preview: ProfilePreview) -> some View {
HStack {
Text(header.name)
Text(preview.name)
Spacer()
ThemeImage(tunnel.statusImageName)
.opaque(header.id == tunnel.currentProfile?.id)
.opaque(preview.id == tunnel.currentProfile?.id)
}
.font(.headline)
}

View File

@ -147,9 +147,9 @@ extension ProfileManager {
!filteredProfiles.isEmpty
}
public var headers: [ProfileHeader] {
public var previews: [ProfilePreview] {
filteredProfiles.map {
$0.header()
processor?.preview(from: $0) ?? ProfilePreview($0)
}
}

View File

@ -0,0 +1,47 @@
//
// ProfilePreview.swift
// Passepartout
//
// Created by Davide De Rosa on 11/22/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 Foundation
import PassepartoutKit
public struct ProfilePreview: Identifiable, Hashable {
public let id: Profile.ID
public let name: String
public let subtitle: String?
public init(id: Profile.ID, name: String, subtitle: String? = nil) {
self.id = id
self.name = name
self.subtitle = subtitle
}
public init(_ profile: Profile) {
id = profile.id
name = profile.name
subtitle = nil
}
}

View File

@ -24,15 +24,14 @@
//
import Foundation
import PassepartoutKit
public struct TunnelInstallation {
public let header: ProfileHeader
public let preview: ProfilePreview
public let onDemand: Bool
public init(header: ProfileHeader, onDemand: Bool) {
self.header = header
public init(preview: ProfilePreview, onDemand: Bool) {
self.preview = preview
self.onDemand = onDemand
}
}

View File

@ -33,6 +33,8 @@ public final class InAppProcessor: ObservableObject, Sendable {
private nonisolated let _isIncluded: (IAPManager, Profile) -> Bool
private nonisolated let _preview: (Profile) -> ProfilePreview
private nonisolated let _willRebuild: (IAPManager, Profile.Builder) throws -> Profile.Builder
private nonisolated let _willInstall: (IAPManager, Profile) throws -> Profile
@ -43,6 +45,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
iapManager: IAPManager,
title: @escaping (Profile) -> String,
isIncluded: @escaping (IAPManager, Profile) -> Bool,
preview: @escaping (Profile) -> ProfilePreview,
willRebuild: @escaping (IAPManager, Profile.Builder) throws -> Profile.Builder,
willInstall: @escaping (IAPManager, Profile) throws -> Profile,
verify: @escaping (IAPManager, Profile) -> Set<AppFeature>?
@ -50,6 +53,7 @@ public final class InAppProcessor: ObservableObject, Sendable {
self.iapManager = iapManager
_title = title
_isIncluded = isIncluded
_preview = preview
_willRebuild = willRebuild
_willInstall = willInstall
_verify = verify
@ -67,6 +71,10 @@ extension InAppProcessor: ProfileProcessor {
_isIncluded(iapManager, profile)
}
public func preview(from profile: Profile) -> ProfilePreview {
_preview(profile)
}
public func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
try _willRebuild(iapManager, builder)
}

View File

@ -29,6 +29,8 @@ import PassepartoutKit
public protocol ProfileProcessor {
func isIncluded(_ profile: Profile) -> Bool
func preview(from profile: Profile) -> ProfilePreview
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder
func verify(_ profile: Profile) -> Set<AppFeature>?

View File

@ -30,7 +30,7 @@ import PassepartoutKit
@MainActor
extension ProfileManager {
public func removeProfiles(at offsets: IndexSet) async {
let idsToRemove = headers
let idsToRemove = previews
.enumerated()
.filter {
offsets.contains($0.offset)

View File

@ -33,12 +33,12 @@ extension TunnelInstallationProviding {
guard let currentProfile = tunnel.currentProfile else {
return nil
}
guard let header = profileManager.headers.first(where: {
guard let preview = profileManager.previews.first(where: {
$0.id == currentProfile.id
}) else {
return nil
}
return TunnelInstallation(header: header, onDemand: currentProfile.onDemand)
return TunnelInstallation(preview: preview, onDemand: currentProfile.onDemand)
}
public var currentProfile: Profile? {

View File

@ -27,6 +27,42 @@ import CommonUtils
import Foundation
import PassepartoutKit
extension Profile: StyledOptionalLocalizableEntity {
public enum OptionalStyle {
case moduleTypes
case connectionType
case nonConnectionTypes
}
public func localizedDescription(optionalStyle: OptionalStyle) -> String? {
switch optionalStyle {
case .moduleTypes:
return activeModules
.nilIfEmpty?
.map(\.moduleType.localizedDescription)
.sorted()
.joined(separator: ", ")
case .connectionType:
return firstConnectionModule(ifActive: true)?
.moduleType
.localizedDescription
case .nonConnectionTypes:
return activeModules
.filter {
!($0 is ConnectionModule)
}
.nilIfEmpty?
.map(\.moduleType.localizedDescription)
.sorted()
.joined(separator: ", ")
}
}
}
extension TunnelStatus: LocalizableEntity {
public var localizedDescription: String {
let V = Strings.Entities.TunnelStatus.self

View File

@ -809,10 +809,8 @@ public enum Strings {
}
}
public enum Rows {
/// %d modules
public static func modules(_ p1: Int) -> String {
return Strings.tr("Localizable", "views.profiles.rows.modules", p1, fallback: "%d modules")
}
/// No active modules
public static let noModules = Strings.tr("Localizable", "views.profiles.rows.no_modules", fallback: "No active modules")
/// Select a profile
public static let notInstalled = Strings.tr("Localizable", "views.profiles.rows.not_installed", fallback: "Select a profile")
}

View File

@ -53,6 +53,9 @@ extension AppContext {
isIncluded: { _, _ in
true
},
preview: {
ProfilePreview($0)
},
willRebuild: { _, builder in
builder
},

View File

@ -117,7 +117,7 @@
// MARK: - Views
"views.profiles.rows.not_installed" = "Select a profile";
"views.profiles.rows.modules" = "%d modules";
"views.profiles.rows.no_modules" = "No active modules";
"views.profiles.folders.active_profile" = "Installed profile";
"views.profiles.folders.default" = "My profiles";
"views.profiles.folders.add_profile" = "Add profile";

View File

@ -47,7 +47,7 @@ extension ProfileImporterTests {
try await sut.tryImport(urls: [], profileManager: profileManager, registry: registry)
XCTAssertEqual(sut.nextURL, nil)
XCTAssertTrue(profileManager.headers.isEmpty)
XCTAssertTrue(profileManager.previews.isEmpty)
}
func test_givenURL_whenImport_thenOneProfileIsImported() async throws {

View File

@ -47,6 +47,10 @@ final class MockProfileProcessor: ProfileProcessor {
return isIncludedBlock(profile)
}
func preview(from profile: Profile) -> ProfilePreview {
ProfilePreview(profile)
}
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
willRebuildCount += 1
return builder

View File

@ -47,7 +47,7 @@ extension ProfileManagerTests {
let sut = ProfileManager(profiles: [profile])
XCTAssertFalse(sut.isReady)
XCTAssertFalse(sut.hasProfiles)
XCTAssertTrue(sut.headers.isEmpty)
XCTAssertTrue(sut.previews.isEmpty)
}
func test_givenRepository_whenNotReady_thenHasNoProfiles() {
@ -55,7 +55,7 @@ extension ProfileManagerTests {
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
XCTAssertFalse(sut.isReady)
XCTAssertFalse(sut.hasProfiles)
XCTAssertTrue(sut.headers.isEmpty)
XCTAssertTrue(sut.previews.isEmpty)
}
func test_givenRepository_whenReady_thenHasProfiles() async throws {
@ -66,7 +66,7 @@ extension ProfileManagerTests {
try await waitForReady(sut)
XCTAssertTrue(sut.isReady)
XCTAssertTrue(sut.hasProfiles)
XCTAssertEqual(sut.headers.count, 1)
XCTAssertEqual(sut.previews.count, 1)
XCTAssertEqual(sut.profile(withId: profile.id), profile)
}
@ -79,15 +79,15 @@ extension ProfileManagerTests {
try await waitForReady(sut)
XCTAssertTrue(sut.isReady)
XCTAssertTrue(sut.hasProfiles)
XCTAssertEqual(sut.headers.count, 2)
XCTAssertEqual(sut.previews.count, 2)
try await wait(sut) {
$0.search(byName: "ar")
} until: {
$0.headers.count == 1
$0.previews.count == 1
}
XCTAssertTrue(sut.isSearching)
let found = try XCTUnwrap(sut.headers.last)
let found = try XCTUnwrap(sut.previews.last)
XCTAssertEqual(found.id, profile2.id)
}
@ -123,8 +123,8 @@ extension ProfileManagerTests {
try await waitForReady(sut)
XCTAssertTrue(sut.isReady)
XCTAssertEqual(sut.headers.count, 1)
XCTAssertEqual(sut.headers.first?.name, "local2")
XCTAssertEqual(sut.previews.count, 1)
XCTAssertEqual(sut.previews.first?.name, "local2")
}
func test_givenRepositoryAndProcessor_whenRequiredFeaturesChange_thenMustReload() async throws {
@ -170,7 +170,7 @@ extension ProfileManagerTests {
} until: {
$0.hasProfiles
}
XCTAssertEqual(sut.headers.count, 1)
XCTAssertEqual(sut.previews.count, 1)
XCTAssertEqual(sut.profile(withId: profile.id), profile)
}
@ -181,7 +181,7 @@ extension ProfileManagerTests {
try await waitForReady(sut)
XCTAssertTrue(sut.isReady)
XCTAssertEqual(sut.headers.first?.id, profile.id)
XCTAssertEqual(sut.previews.first?.id, profile.id)
var builder = profile.builder()
builder.name = "newName"
@ -190,7 +190,7 @@ extension ProfileManagerTests {
try await wait(sut) {
try await $0.save(renamedProfile)
} until: {
$0.headers.first?.name == renamedProfile.name
$0.previews.first?.name == renamedProfile.name
}
}
@ -264,7 +264,7 @@ extension ProfileManagerTests {
} until: {
!$0.hasProfiles
}
XCTAssertTrue(sut.headers.isEmpty)
XCTAssertTrue(sut.previews.isEmpty)
}
}
@ -336,7 +336,7 @@ extension ProfileManagerTests {
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
try await waitForReady(sut)
XCTAssertEqual(sut.headers.count, 1)
XCTAssertEqual(sut.previews.count, 1)
let newProfile = sut.new(withName: profile.name)
XCTAssertEqual(newProfile.name, "example.1")
@ -352,22 +352,22 @@ extension ProfileManagerTests {
try await wait(sut) {
try await $0.duplicate(profileWithId: profile.id)
} until: {
$0.headers.count == 2
$0.previews.count == 2
}
try await wait(sut) {
try await $0.duplicate(profileWithId: profile.id)
} until: {
$0.headers.count == 3
$0.previews.count == 3
}
try await wait(sut) {
try await $0.duplicate(profileWithId: profile.id)
} until: {
$0.headers.count == 4
$0.previews.count == 4
}
XCTAssertEqual(sut.headers.map(\.name), [
XCTAssertEqual(sut.previews.map(\.name), [
"example",
"example.1",
"example.2",
@ -400,10 +400,10 @@ extension ProfileManagerTests {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
$0.headers.count == allProfiles.count
$0.previews.count == allProfiles.count
}
XCTAssertEqual(Set(sut.headers), Set(allProfiles.map { $0.header() }))
XCTAssertEqual(Set(sut.previews), Set(allProfiles.map { ProfilePreview($0) }))
localProfiles.forEach {
XCTAssertFalse(sut.isRemotelyShared(profileWithId: $0.id))
}
@ -437,10 +437,10 @@ extension ProfileManagerTests {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
$0.headers.count == 4 // unique IDs
$0.previews.count == 4 // unique IDs
}
sut.headers.forEach {
sut.previews.forEach {
switch $0.id {
case l1:
XCTAssertEqual($0.name, "remote1")
@ -493,7 +493,7 @@ extension ProfileManagerTests {
}
XCTAssertEqual(processor.isIncludedCount, allProfiles.count)
XCTAssertEqual(Set(sut.headers), Set(localProfiles.map { $0.header() }))
XCTAssertEqual(Set(sut.previews), Set(localProfiles.map { ProfilePreview($0) }))
localProfiles.forEach {
XCTAssertFalse(sut.isRemotelyShared(profileWithId: $0.id))
}
@ -533,7 +533,7 @@ extension ProfileManagerTests {
didImport
}
try sut.headers.forEach {
try sut.previews.forEach {
let profile = try XCTUnwrap(sut.profile(withId: $0.id))
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
switch $0.id {
@ -564,7 +564,7 @@ extension ProfileManagerTests {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
$0.headers.count == localProfiles.count
$0.previews.count == localProfiles.count
}
let r1 = UUID()
@ -588,7 +588,7 @@ extension ProfileManagerTests {
newProfile("remote3", id: r3, fingerprint: fp3)
]
} until: {
$0.headers.count == 5
$0.previews.count == 5
}
localProfiles.forEach {
@ -627,7 +627,7 @@ extension ProfileManagerTests {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
$0.headers.count == 1
$0.previews.count == 1
}
try await wait(sut) { _ in
remoteRepository.profiles = []
@ -635,8 +635,8 @@ extension ProfileManagerTests {
didImport
}
XCTAssertEqual(sut.headers.count, 1)
XCTAssertEqual(sut.headers.first, profile.header())
XCTAssertEqual(sut.previews.count, 1)
XCTAssertEqual(sut.previews.first, ProfilePreview(profile))
}
func test_givenRemoteRepositoryAndMirroring_whenRemoteIsDeleted_thenLocalIsDeleted() async throws {
@ -656,7 +656,7 @@ extension ProfileManagerTests {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
$0.headers.count == 1
$0.previews.count == 1
}
try await wait(sut) { _ in
remoteRepository.profiles = []

View File

@ -45,6 +45,13 @@ extension IAPManager {
isIncluded: {
Configuration.ProfileManager.isIncluded($0, $1)
},
preview: {
ProfilePreview(
id: $0.id,
name: $0.name,
subtitle: $0.localizedDescription(optionalStyle: .moduleTypes)
)
},
willRebuild: { _, builder in
builder
},