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:
parent
a2f246454d
commit
4b4fca8344
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import Foundation
|
|||
import PassepartoutKit
|
||||
|
||||
struct ProfileFlow {
|
||||
let onEditProfile: (ProfileHeader) -> Void
|
||||
let onEditProfile: (ProfilePreview) -> Void
|
||||
|
||||
let onEditProviderEntity: (Profile) -> Void
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -53,6 +53,9 @@ extension AppContext {
|
|||
isIncluded: { _, _ in
|
||||
true
|
||||
},
|
||||
preview: {
|
||||
ProfilePreview($0)
|
||||
},
|
||||
willRebuild: { _, builder in
|
||||
builder
|
||||
},
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue