Move more views to UILibrary (#919)

- Move about subviews to UILibrary
- Refactor about to single coordinator + platform views
- Refactor debug log to single view + content views
- Take out debug log routes from about routes
- Rename Settings* to Preferences*
- Reuse empty modifier in debug log
- Fix a visual bug in .themeTrailingValue() (extra Spacer)

Preparation for #914
This commit is contained in:
Davide 2024-11-23 19:26:33 +01:00 committed by GitHub
parent a301806ac7
commit 2a46173169
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 472 additions and 440 deletions

View File

@ -65,7 +65,7 @@ extension PassepartoutApp {
.defaultSize(width: 600, height: 400)
Settings {
SettingsView(profileManager: context.profileManager)
PreferencesView(profileManager: context.profileManager)
.frame(minWidth: 300, minHeight: 300)
.withEnvironment(from: context, theme: theme)
.environmentObject(settings)

View File

@ -0,0 +1,139 @@
//
// AboutCoordinator.swift
// Passepartout
//
// Created by Davide De Rosa on 8/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 CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
struct AboutCoordinator: View {
@EnvironmentObject
private var iapManager: IAPManager
@Environment(\.dismiss)
private var dismiss
let profileManager: ProfileManager
let tunnel: ExtendedTunnel
@State
private var path = NavigationPath()
@State
private var navigationRoute: AboutCoordinatorRoute?
var body: some View {
AboutContentView(
profileManager: profileManager,
isRestricted: iapManager.isRestricted,
path: $path,
navigationRoute: $navigationRoute,
linkContent: linkView(to:),
aboutDestination: pushDestination(for:),
logDestination: pushDestination(for:)
)
}
}
extension AboutCoordinator {
func linkView(to route: AboutCoordinatorRoute) -> some View {
NavigationLink(title(for: route), value: route)
}
func title(for route: AboutCoordinatorRoute) -> String {
switch route {
case .credits:
return Strings.Views.About.Credits.title
case .diagnostics:
return Strings.Views.Diagnostics.title
case .donate:
return Strings.Views.Donate.title
case .links:
return Strings.Views.About.Links.title
}
}
@ViewBuilder
func pushDestination(for item: AboutCoordinatorRoute?) -> some View {
switch item {
case .credits:
CreditsView()
case .diagnostics:
DiagnosticsView(profileManager: profileManager, tunnel: tunnel)
case .donate:
DonateView()
case .links:
LinksView()
default:
Text(Strings.Global.noSelection)
.themeEmptyMessage()
}
}
@ViewBuilder
func pushDestination(for item: DebugLogRoute?) -> some View {
switch item {
case .app(let title):
DebugLogView(withAppParameters: Constants.shared.log) {
DebugLogContentView(lines: $0)
}
.navigationTitle(title)
case .tunnel(let title, let url):
if let url {
DebugLogView(withURL: url) {
DebugLogContentView(lines: $0)
}
.navigationTitle(title)
} else {
DebugLogView(withTunnel: tunnel, parameters: Constants.shared.log) {
DebugLogContentView(lines: $0)
}
.navigationTitle(title)
}
default:
Text(Strings.Global.noSelection)
.themeEmptyMessage()
}
}
}
#Preview {
AboutCoordinator(
profileManager: .mock,
tunnel: .mock
)
.withMockEnvironment()
}

View File

@ -1,8 +1,8 @@
//
// AboutRouterView+iOS.swift
// AboutCoordinatorRoute.swift
// Passepartout
//
// Created by Davide De Rosa on 8/26/24.
// Created by Davide De Rosa on 11/23/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@ -23,21 +23,14 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
#if os(iOS)
import Foundation
import CommonLibrary
import SwiftUI
enum AboutCoordinatorRoute: Hashable {
case credits
extension AboutRouterView {
var body: some View {
AboutView(
profileManager: profileManager,
navigationRoute: $navigationRoute
)
.navigationDestination(for: NavigationRoute.self, destination: pushDestination)
.themeNavigationDetail()
.themeNavigationStack(closable: true, path: $path)
}
case diagnostics
case donate
case links
}
#endif

View File

@ -1,105 +0,0 @@
//
// AboutRouterView.swift
// Passepartout
//
// Created by Davide De Rosa on 8/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 CommonLibrary
import PassepartoutKit
import SwiftUI
struct AboutRouterView: View {
@Environment(\.dismiss)
var dismiss
let profileManager: ProfileManager
let tunnel: ExtendedTunnel
@State
var path = NavigationPath()
@State
var navigationRoute: NavigationRoute?
}
extension AboutRouterView {
enum NavigationRoute: Hashable {
case appDebugLog(title: String)
case credits
case diagnostics
case donate
case links
case tunnelDebugLog(title: String, url: URL?)
}
@ViewBuilder
func pushDestination(for item: NavigationRoute?) -> some View {
switch item {
case .appDebugLog(let title):
DebugLogView.withApp(parameters: Constants.shared.log)
.navigationTitle(title)
case .credits:
CreditsView()
case .diagnostics:
DiagnosticsView(
profileManager: profileManager,
tunnel: tunnel
)
case .donate:
DonateView()
case .links:
LinksView()
case .tunnelDebugLog(let title, let url):
if let url {
DebugLogView.withURL(url)
.navigationTitle(title)
} else {
DebugLogView.withTunnel(tunnel, parameters: Constants.shared.log)
.navigationTitle(title)
}
default:
Text(Strings.Global.noSelection)
.themeEmptyMessage()
}
}
}
#Preview {
AboutRouterView(
profileManager: .mock,
tunnel: .mock
)
.withMockEnvironment()
}

View File

@ -1,76 +0,0 @@
//
// AboutView.swift
// Passepartout
//
// Created by Davide De Rosa on 8/23/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 CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
struct AboutView: View {
@EnvironmentObject
var iapManager: IAPManager
let profileManager: ProfileManager
@Binding
var navigationRoute: AboutRouterView.NavigationRoute?
var body: some View {
listView
}
}
extension AboutView {
var creditsLink: some View {
navLink(Strings.Views.About.Credits.title, to: .credits)
}
var diagnosticsLink: some View {
navLink(Strings.Views.Diagnostics.title, to: .diagnostics)
}
var donateLink: some View {
navLink(Strings.Views.Donate.title, to: .donate)
}
var linksLink: some View {
navLink(Strings.Views.About.Links.title, to: .links)
}
}
private extension AboutView {
func navLink(_ title: String, to route: AboutRouterView.NavigationRoute) -> some View {
NavigationLink(title, value: route)
}
}
#Preview {
AboutView(
profileManager: .mock,
navigationRoute: .constant(nil)
)
.withMockEnvironment()
}

View File

@ -1,5 +1,5 @@
//
// AboutView+iOS.swift
// AboutContentView+iOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/27/24.
@ -25,23 +25,55 @@
#if os(iOS)
import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
extension AboutView {
struct AboutContentView<LinkContent, AboutDestination, LogDestination>: View where LinkContent: View, AboutDestination: View, LogDestination: View {
@Environment(\.dismiss)
private var dismiss
let profileManager: ProfileManager
let isRestricted: Bool
@Binding
var path: NavigationPath
@Binding
var navigationRoute: AboutCoordinatorRoute?
let linkContent: (AboutCoordinatorRoute) -> LinkContent
let aboutDestination: (AboutCoordinatorRoute?) -> AboutDestination
let logDestination: (DebugLogRoute?) -> LogDestination
var body: some View {
listView
.navigationDestination(for: AboutCoordinatorRoute.self, destination: aboutDestination)
.navigationDestination(for: DebugLogRoute.self, destination: logDestination)
.themeNavigationDetail()
.themeNavigationStack(closable: true, path: $path)
}
}
private extension AboutContentView {
var listView: some View {
List {
SettingsSectionGroup(profileManager: profileManager)
PreferencesGroup(profileManager: profileManager)
Group {
linksLink
creditsLink
if !iapManager.isRestricted {
donateLink
linkContent(.links)
linkContent(.credits)
if !isRestricted {
linkContent(.donate)
}
}
.themeSection(header: Strings.Views.About.Sections.resources)
Section {
diagnosticsLink
linkContent(.diagnostics)
Text(Strings.Global.version)
.themeTrailingValue(BundleConfiguration.mainVersionString)
}

View File

@ -0,0 +1,95 @@
//
// AboutContentView+macOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/27/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/>.
//
#if os(macOS)
import CommonLibrary
import PassepartoutKit
import SwiftUI
struct AboutContentView<LinkContent, AboutDestination, LogDestination>: View where LinkContent: View, AboutDestination: View, LogDestination: View {
@Environment(\.dismiss)
private var dismiss
let profileManager: ProfileManager
let isRestricted: Bool
@Binding
var path: NavigationPath
@Binding
var navigationRoute: AboutCoordinatorRoute?
let linkContent: (AboutCoordinatorRoute) -> LinkContent
let aboutDestination: (AboutCoordinatorRoute?) -> AboutDestination
let logDestination: (DebugLogRoute?) -> LogDestination
var body: some View {
NavigationSplitView {
listView
} detail: {
aboutDestination(navigationRoute)
.navigationDestination(for: AboutCoordinatorRoute.self, destination: aboutDestination)
.navigationDestination(for: DebugLogRoute.self, destination: logDestination)
.themeNavigationStack(closable: false, path: $path)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(Strings.Global.ok) {
dismiss()
}
}
}
}
.onLoad {
navigationRoute = .links
}
}
}
private extension AboutContentView {
var listView: some View {
List(selection: $navigationRoute) {
Section {
linkContent(.links)
linkContent(.credits)
if !isRestricted {
linkContent(.donate)
}
linkContent(.diagnostics)
}
}
.safeAreaInset(edge: .bottom) {
Text(BundleConfiguration.mainVersionString)
.padding(.bottom)
}
.navigationTitle(Strings.Views.About.title)
}
}
#endif

View File

@ -1,56 +0,0 @@
//
// AboutRouterView+macOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/26/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/>.
//
#if os(macOS)
import CommonLibrary
import SwiftUI
extension AboutRouterView {
var body: some View {
NavigationSplitView {
AboutView(
profileManager: profileManager,
navigationRoute: $navigationRoute
)
} detail: {
pushDestination(for: navigationRoute)
.navigationDestination(for: NavigationRoute.self, destination: pushDestination)
.themeNavigationStack(closable: false, path: $path)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(Strings.Global.ok) {
dismiss()
}
}
}
}
.onLoad {
navigationRoute = .links
}
}
}
#endif

View File

@ -1,51 +0,0 @@
//
// AboutView+macOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/27/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/>.
//
#if os(macOS)
import PassepartoutKit
import SwiftUI
extension AboutView {
var listView: some View {
List(selection: $navigationRoute) {
Section {
linksLink
creditsLink
if !iapManager.isRestricted {
donateLink
}
diagnosticsLink
}
}
.safeAreaInset(edge: .bottom) {
Text(BundleConfiguration.mainVersionString)
.padding(.bottom)
}
.navigationTitle(Strings.Views.About.title)
}
}
#endif

View File

@ -27,6 +27,7 @@ import CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
import UILibrary
public struct AppCoordinator: View, AppCoordinatorConforming {
@ -99,7 +100,7 @@ extension AppCoordinator {
case migrateProfiles
case settings
case preferences
var id: Int {
switch self {
@ -107,7 +108,7 @@ extension AppCoordinator {
case .editProfile: return 2
case .editProviderEntity: return 3
case .migrateProfiles: return 4
case .settings: return 5
case .preferences: return 5
}
}
@ -190,8 +191,8 @@ extension AppCoordinator {
profileManager: profileManager,
layout: $layout,
isImporting: $isImporting,
onSettings: {
present(.settings)
onPreferences: {
present(.preferences)
},
onAbout: {
present(.about)
@ -207,7 +208,7 @@ extension AppCoordinator {
func modalDestination(for item: ModalRoute?) -> some View {
switch item {
case .about:
AboutRouterView(
AboutCoordinator(
profileManager: profileManager,
tunnel: tunnel
)
@ -240,8 +241,8 @@ extension AppCoordinator {
)
.themeNavigationStack(closable: true, path: $migrationPath)
case .settings:
SettingsView(profileManager: profileManager)
case .preferences:
PreferencesView(profileManager: profileManager)
default:
EmptyView()

View File

@ -44,7 +44,7 @@ struct AppToolbar: ToolbarContent, SizeClassProviding {
@Binding
var isImporting: Bool
let onSettings: () -> Void
let onPreferences: () -> Void
let onAbout: () -> Void
@ -81,8 +81,8 @@ private extension AppToolbar {
)
}
var settingsButton: some View {
Button(action: onSettings) {
var preferencesButton: some View {
Button(action: onPreferences) {
ThemeImage(.settings)
}
}
@ -110,7 +110,7 @@ private extension AppToolbar {
profileManager: .mock,
layout: .constant(.list),
isImporting: .constant(false),
onSettings: {},
onPreferences: {},
onAbout: {},
onMigrateProfiles: {},
onNewProfile: { _ in }

View File

@ -265,7 +265,7 @@ private struct CardModifier: ViewModifier {
#if os(iOS)
content
.padding(.vertical)
#elseif os(macOS)
#else
content
#endif

View File

@ -0,0 +1,32 @@
//
// DebugLogRoute.swift
// Passepartout
//
// Created by Davide De Rosa on 11/23/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
enum DebugLogRoute: Hashable {
case app(title: String)
case tunnel(title: String, url: URL?)
}

View File

@ -102,8 +102,8 @@ struct DiagnosticsView: View {
private extension DiagnosticsView {
var liveLogSection: some View {
Group {
navLink(Strings.Views.Diagnostics.Rows.app, to: .appDebugLog(title: Strings.Views.Diagnostics.Rows.app))
navLink(Strings.Views.Diagnostics.Rows.tunnel, to: .tunnelDebugLog(title: Strings.Views.Diagnostics.Rows.tunnel, url: nil))
navLink(Strings.Views.Diagnostics.Rows.app, to: .app(title: Strings.Views.Diagnostics.Rows.app))
navLink(Strings.Views.Diagnostics.Rows.tunnel, to: .tunnel(title: Strings.Views.Diagnostics.Rows.tunnel, url: nil))
Toggle(Strings.Views.Diagnostics.Rows.includePrivateData, isOn: $logsPrivateData)
.onChange(of: logsPrivateData) {
@ -156,13 +156,13 @@ private extension DiagnosticsView {
func logView(for item: LogEntry) -> some View {
ThemeRemovableItemRow(isEditing: true) {
let dateString = dateFormatter.string(from: item.date)
navLink(dateString, to: .tunnelDebugLog(title: dateString, url: item.url))
navLink(dateString, to: .tunnel(title: dateString, url: item.url))
} removeAction: {
removeTunnelLog(at: item.url)
}
}
func navLink(_ title: String, to value: AboutRouterView.NavigationRoute) -> some View {
func navLink(_ title: String, to value: DebugLogRoute) -> some View {
NavigationLink(title, value: value)
}
}

View File

@ -1,5 +1,5 @@
//
// DebugLogView+iOS.swift
// DebugLogContentView+iOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/31/24.
@ -27,9 +27,11 @@
import SwiftUI
extension DebugLogView {
var contentView: some View {
TextEditor(text: .constant(content))
struct DebugLogContentView: View {
let lines: [String]
var body: some View {
TextEditor(text: .constant(lines.joined(separator: "\n")))
.font(.caption)
.monospaced()
}

View File

@ -1,5 +1,5 @@
//
// DebugLogView+macOS.swift
// DebugLogContentView+macOS.swift
// Passepartout
//
// Created by Davide De Rosa on 8/31/24.
@ -27,10 +27,12 @@
import SwiftUI
extension DebugLogView {
var contentView: some View {
struct DebugLogContentView: View {
let lines: [String]
var body: some View {
List {
ForEach(Array(currentLines.enumerated()), id: \.offset) {
ForEach(Array(lines.enumerated()), id: \.offset) {
Text($0.element)
.monospaced()
}

View File

@ -104,7 +104,7 @@ private extension ProfileCoordinator {
)
.themeNavigationDetail()
.themeNavigationStack(if: modally, path: $path)
#elseif os(macOS)
#else
ProfileSplitView(
profileEditor: profileEditor,
moduleViewFactory: moduleViewFactory,

View File

@ -214,6 +214,10 @@ extension View {
modifier(ThemeProgressViewModifier(isProgressing: isProgressing, isEmpty: isEmpty, emptyContent: emptyContent))
}
public func themeTrailingValue(_ value: CustomStringConvertible?, truncationMode: Text.TruncationMode = .tail) -> some View {
modifier(ThemeTrailingValueModifier(value: value, truncationMode: truncationMode))
}
#if !os(tvOS)
public func themeWindow(width: CGFloat, height: CGFloat) -> some View {
modifier(ThemeWindowModifier(size: .init(width: width, height: height)))
@ -223,10 +227,6 @@ extension View {
modifier(ThemePlainButtonModifier(action: action))
}
public func themeTrailingValue(_ value: CustomStringConvertible?, truncationMode: Text.TruncationMode = .tail) -> some View {
modifier(ThemeTrailingValueModifier(value: value, truncationMode: truncationMode))
}
public func themeGridHeader(title: String?) -> some View {
modifier(ThemeGridSectionModifier(title: title))
}
@ -479,16 +479,6 @@ struct ThemeProgressViewModifier<EmptyContent>: ViewModifier where EmptyContent:
}
}
#if !os(tvOS)
struct ThemeWindowModifier: ViewModifier {
let size: CGSize
}
struct ThemePlainButtonModifier: ViewModifier {
let action: () -> Void
}
struct ThemeTrailingValueModifier: ViewModifier {
let value: CustomStringConvertible?
@ -497,7 +487,6 @@ struct ThemeTrailingValueModifier: ViewModifier {
func body(content: Content) -> some View {
LabeledContent {
if let value {
Spacer()
Text(value.description)
.foregroundStyle(.secondary)
.lineLimit(1)
@ -509,6 +498,16 @@ struct ThemeTrailingValueModifier: ViewModifier {
}
}
#if !os(tvOS)
struct ThemeWindowModifier: ViewModifier {
let size: CGSize
}
struct ThemePlainButtonModifier: ViewModifier {
let action: () -> Void
}
struct ThemeGridSectionModifier: ViewModifier {
@EnvironmentObject

View File

@ -27,8 +27,11 @@ import CommonLibrary
import CommonUtils
import SwiftUI
struct CreditsView: View {
var body: some View {
public struct CreditsView: View {
public init() {
}
public var body: some View {
GenericCreditsView(
credits: Self.credits,
licensesHeader: Strings.Views.About.Credits.licenses,

View File

@ -27,7 +27,7 @@ import CommonLibrary
import CommonUtils
import SwiftUI
struct DonateView: View {
public struct DonateView: View {
@EnvironmentObject
private var iapManager: IAPManager
@ -50,7 +50,10 @@ struct DonateView: View {
@StateObject
private var errorHandler: ErrorHandler = .default()
var body: some View {
public init() {
}
public var body: some View {
donationsView
.themeProgress(if: isFetchingProducts)
.navigationTitle(title)

View File

@ -27,8 +27,11 @@ import CommonLibrary
import PassepartoutKit
import SwiftUI
struct LinksView: View {
var body: some View {
public struct LinksView: View {
public init() {
}
public var body: some View {
Form {
supportSection
webSection

View File

@ -28,55 +28,21 @@ import CommonUtils
import PassepartoutKit
import SwiftUI
extension DebugLogView {
static func withApp(parameters: Constants.Log) -> DebugLogView {
DebugLogView {
PassepartoutConfiguration.shared.currentLog(parameters: parameters)
}
}
public struct DebugLogView<Content>: View where Content: View {
private let fetchLines: () async -> [String]
static func withTunnel(_ tunnel: ExtendedTunnel, parameters: Constants.Log) -> DebugLogView {
DebugLogView {
await tunnel.currentLog(parameters: parameters)
}
}
static func withURL(_ url: URL) -> DebugLogView {
DebugLogView {
do {
return try String(contentsOf: url)
.split(separator: "\n")
.map(String.init)
} catch {
return []
}
}
}
}
struct DebugLogView: View {
let fetchLines: () async -> [String]
private let content: ([String]) -> Content
@State
private(set) var currentLines: [String] = []
var body: some View {
ZStack {
if !currentLines.isEmpty {
contentView
} else {
Text(Strings.Global.noContent)
.themeEmptyMessage()
public var body: some View {
content(currentLines)
.themeEmpty(if: currentLines.isEmpty, message: Strings.Global.noContent)
.toolbar(content: toolbarContent)
.task {
currentLines = await fetchLines()
}
}
.toolbar(content: toolbarContent)
.task {
currentLines = await fetchLines()
}
}
var content: String {
currentLines.joined(separator: "\n")
}
}
@ -92,7 +58,7 @@ private extension DebugLogView {
var copyButton: some View {
Button {
Utils.copyToPasteboard(content)
Utils.copyToPasteboard(currentLines.joined(separator: "\n"))
} label: {
ThemeImage(.copy)
}
@ -104,3 +70,47 @@ private extension DebugLogView {
// ShareLink(item: content)
// }
}
// MARK: - Shortcuts
extension DebugLogView {
public init(
withAppParameters parameters: Constants.Log,
content: @escaping ([String]) -> Content
) {
self.init {
PassepartoutConfiguration.shared.currentLog(parameters: parameters)
} content: {
content($0)
}
}
public init(
withTunnel tunnel: ExtendedTunnel,
parameters: Constants.Log,
content: @escaping ([String]) -> Content
) {
self.init {
await tunnel.currentLog(parameters: parameters)
} content: {
content($0)
}
}
public init(
withURL url: URL,
content: @escaping ([String]) -> Content
) {
self.init {
do {
return try String(contentsOf: url)
.split(separator: "\n")
.map(String.init)
} catch {
return []
}
} content: {
content($0)
}
}
}

View File

@ -1,5 +1,5 @@
//
// SettingsSectionGroup.swift
// PreferencesGroup.swift
// Passepartout
//
// Created by Davide De Rosa on 10/3/24.
@ -28,27 +28,32 @@ import CommonUtils
import PassepartoutKit
import SwiftUI
struct SettingsSectionGroup: View {
let profileManager: ProfileManager
public struct PreferencesGroup: View {
#if os(iOS)
@AppStorage(AppPreference.locksInBackground.key)
private var locksInBackground = false
#else
#elseif os(macOS)
@EnvironmentObject
private var settings: MacSettingsModel
#endif
private let profileManager: ProfileManager
@State
private var isConfirmingEraseiCloud = false
@State
private var isErasingiCloud = false
var body: some View {
public init(profileManager: ProfileManager) {
self.profileManager = profileManager
}
public var body: some View {
#if os(iOS)
lockInBackgroundToggle
#else
#elseif os(macOS)
launchesOnLoginToggle
keepsInMenuToggle
#endif
@ -56,13 +61,13 @@ struct SettingsSectionGroup: View {
}
}
private extension SettingsSectionGroup {
private extension PreferencesGroup {
#if os(iOS)
var lockInBackgroundToggle: some View {
Toggle(Strings.Views.Settings.locksInBackground, isOn: $locksInBackground)
.themeSectionWithSingleRow(footer: Strings.Views.Settings.LocksInBackground.footer)
}
#else
#elseif os(macOS)
var launchesOnLoginToggle: some View {
Toggle(Strings.Views.Settings.launchesOnLogin, isOn: $settings.launchesOnLogin)
.themeSectionWithSingleRow(footer: Strings.Views.Settings.LaunchesOnLogin.footer)

View File

@ -1,5 +1,5 @@
//
// SettingsView.swift
// PreferencesView.swift
// Passepartout
//
// Created by Davide De Rosa on 9/28/24.
@ -26,8 +26,8 @@
import CommonLibrary
import SwiftUI
public struct SettingsView: View {
let profileManager: ProfileManager
public struct PreferencesView: View {
private let profileManager: ProfileManager
@State
private var path = NavigationPath()
@ -38,7 +38,7 @@ public struct SettingsView: View {
public var body: some View {
Form {
SettingsSectionGroup(profileManager: profileManager)
PreferencesGroup(profileManager: profileManager)
}
.themeForm()
.navigationTitle(Strings.Global.settings)

View File

@ -81,7 +81,7 @@ extension ProfileManagerTests {
XCTAssertTrue(sut.hasProfiles)
XCTAssertEqual(sut.previews.count, 2)
try await wait(sut) {
try await wait(sut, "Search") {
$0.search(byName: "ar")
} until: {
$0.previews.count == 1
@ -165,7 +165,7 @@ extension ProfileManagerTests {
XCTAssertFalse(sut.hasProfiles)
let profile = newProfile()
try await wait(sut) {
try await wait(sut, "Save") {
try await $0.save(profile)
} until: {
$0.hasProfiles
@ -187,7 +187,7 @@ extension ProfileManagerTests {
builder.name = "newName"
let renamedProfile = try builder.tryBuild()
try await wait(sut) {
try await wait(sut, "Save") {
try await $0.save(renamedProfile)
} until: {
$0.previews.first?.name == renamedProfile.name
@ -259,7 +259,7 @@ extension ProfileManagerTests {
XCTAssertTrue(sut.isReady)
XCTAssertTrue(sut.hasProfiles)
try await wait(sut) {
try await wait(sut, "Remove") {
await $0.remove(withId: profile.id)
} until: {
!$0.hasProfiles
@ -349,19 +349,19 @@ extension ProfileManagerTests {
try await waitForReady(sut)
try await wait(sut) {
try await wait(sut, "Duplicate 1") {
try await $0.duplicate(profileWithId: profile.id)
} until: {
$0.previews.count == 2
}
try await wait(sut) {
try await wait(sut, "Duplicate 2") {
try await $0.duplicate(profileWithId: profile.id)
} until: {
$0.previews.count == 3
}
try await wait(sut) {
try await wait(sut, "Duplicate 3") {
try await $0.duplicate(profileWithId: profile.id)
} until: {
$0.previews.count == 4
@ -396,7 +396,7 @@ extension ProfileManagerTests {
remoteRepository
})
try await wait(sut) {
try await wait(sut, "Remote import") {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
@ -433,7 +433,7 @@ extension ProfileManagerTests {
remoteRepository
})
try await wait(sut) {
try await wait(sut, "Remote import") {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
@ -485,9 +485,9 @@ extension ProfileManagerTests {
observeRemoteImport(sut) {
didImport = true
}
try await waitForReady(sut)
try await wait(sut) { _ in
//
try await wait(sut, "Remote import") {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: { _ in
didImport
}
@ -526,9 +526,9 @@ extension ProfileManagerTests {
observeRemoteImport(sut) {
didImport = true
}
try await waitForReady(sut)
try await wait(sut) { _ in
//
try await wait(sut, "Remote import") {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: { _ in
didImport
}
@ -560,7 +560,7 @@ extension ProfileManagerTests {
remoteRepository
})
try await wait(sut) {
try await wait(sut, "Remote import") {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
@ -574,13 +574,13 @@ extension ProfileManagerTests {
let fp2 = UUID()
let fp3 = UUID()
try await wait(sut) { _ in
try await wait(sut, "Multiple imports") { _ in
remoteRepository.profiles = [
newProfile("remote1", id: r1)
]
remoteRepository.profiles = [
newProfile("remote1", id: r1),
newProfile("remote2", id: r2),
newProfile("remote2", id: r2)
]
remoteRepository.profiles = [
newProfile("remote1", id: r1, fingerprint: fp1),
@ -623,13 +623,13 @@ extension ProfileManagerTests {
observeRemoteImport(sut) {
didImport = true
}
try await wait(sut) {
try await wait(sut, "Remote import") {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
$0.previews.count == 1
}
try await wait(sut) { _ in
try await wait(sut, "Remote reset") { _ in
remoteRepository.profiles = []
} until: { _ in
didImport
@ -652,13 +652,13 @@ extension ProfileManagerTests {
observeRemoteImport(sut) {
didImport = true
}
try await wait(sut) {
try await wait(sut, "Remote import") {
try await $0.observeLocal()
try await $0.observeRemote(true)
} until: {
$0.previews.count == 1
}
try await wait(sut) { _ in
try await wait(sut, "Remote reset") { _ in
remoteRepository.profiles = []
} until: { _ in
didImport
@ -684,7 +684,7 @@ private extension ProfileManagerTests {
}
func waitForReady(_ sut: ProfileManager, importingRemote: Bool = true) async throws {
try await wait(sut) {
try await wait(sut, "Ready") {
try await $0.observeLocal()
try await $0.observeRemote(importingRemote)
} until: {
@ -705,10 +705,11 @@ private extension ProfileManagerTests {
func wait(
_ sut: ProfileManager,
_ description: String,
after action: (ProfileManager) async throws -> Void,
until condition: @escaping (ProfileManager) -> Bool
) async throws {
let exp = expectation(description: "Wait")
let exp = expectation(description: description)
var wasMet = false
sut.objectWillChange
.sink {