From 0047d095fb18f53e4a7dbeb9a7ca2fa4f7ebc672 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Wed, 27 Apr 2022 16:13:01 +0200 Subject: [PATCH] Adjust navigation style to all devices - Mac - Drop all styles - Tweak hide title bar - Hide navigation bar - Restore single section for all profiles - Allows using NavigationLink safely - Indirectly fixes multitasking - Retains selection on profile activation - Clean up presentActiveProfile - Leave active profile in its position - Fixes Mac flashing row selection on profile activation - Unify profile row appearance - Use fixed .headline font - Add subtitles to inactive profiles - Use padding rather than fixed row height CAVEATS: - Do not preselect active profile on iPad launch, as doing so seems to present two ProfileView on top of each other, one from MainView and one from the NavigationLink. - Do not touch .listStyle() of master view, as it seems to break navigation esp. in iPad multitasking. --- Passepartout.xcodeproj/project.pbxproj | 12 +- Passepartout/App/Constants/Theme.swift | 16 +- Passepartout/App/L10n/TunnelKit+L10n.swift | 2 +- Passepartout/App/PassepartoutApp.swift | 1 + .../App/Reusable/View+Extensions.swift | 37 ++++ .../App/Views/OrganizerView+Profiles.swift | 165 +++++++++--------- Passepartout/App/Views/ProfileHeaderRow.swift | 63 ------- Passepartout/App/Views/ProfileRow.swift | 87 +++++++++ Passepartout/App/Views/VPNStatusText.swift | 53 ------ 9 files changed, 230 insertions(+), 206 deletions(-) delete mode 100644 Passepartout/App/Views/ProfileHeaderRow.swift create mode 100644 Passepartout/App/Views/ProfileRow.swift delete mode 100644 Passepartout/App/Views/VPNStatusText.swift diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index f5dc6547..d5b65115 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -92,6 +92,7 @@ 0EBC075B27EC4FFF00208AD9 /* ReportIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */; }; 0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075C27EC529000208AD9 /* DebugLog+Constants.swift */; }; 0EBC076027EC587900208AD9 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */; }; + 0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */; }; 0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF71ED27B6A99300CDB528 /* AccountView.swift */; }; 0ED1D6DC27DBA41700983466 /* DiagnosticsView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED1D6DB27DBA41700983466 /* DiagnosticsView+OpenVPN.swift */; }; 0ED1D6DE27DBA42100983466 /* DiagnosticsView+WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED1D6DD27DBA42100983466 /* DiagnosticsView+WireGuard.swift */; }; @@ -110,7 +111,6 @@ 0ED89C1727DE0E05008B36D6 /* IntentEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */; }; 0ED89C1C27DE3ABC008B36D6 /* ShortcutsView+Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */; }; 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */; }; - 0ED89C2527DE45A3008B36D6 /* ProfileHeaderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */; }; 0EDE02C227F61C79000FBE3C /* EditableTextList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE02C127F61C79000FBE3C /* EditableTextList.swift */; }; 0EE11CD2280D8317003BE431 /* OrganizerView+SettingsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE11CD1280D8317003BE431 /* OrganizerView+SettingsMenu.swift */; }; 0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE8B7E227FF340F00B68621 /* VPNProtocolType+FileExtensions.swift */; }; @@ -122,7 +122,6 @@ 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; }; 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; }; 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; }; - 0EF708322811CC8400A3A308 /* VPNStatusText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF708312811CC8400A3A308 /* VPNStatusText.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -310,6 +309,7 @@ 0EBE2FD62360F89500F0D5AB /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; 0EBE2FD72360F89600F0D5AB /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; 0EBE2FD82360F89600F0D5AB /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; + 0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRow.swift; sourceTree = ""; }; 0ECF71ED27B6A99300CDB528 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; 0ED1D6DB27DBA41700983466 /* DiagnosticsView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiagnosticsView+OpenVPN.swift"; sourceTree = ""; }; 0ED1D6DD27DBA42100983466 /* DiagnosticsView+WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiagnosticsView+WireGuard.swift"; sourceTree = ""; }; @@ -325,7 +325,6 @@ 0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentEditView.swift; sourceTree = ""; }; 0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShortcutsView+Add.swift"; sourceTree = ""; }; 0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentAddView.swift; sourceTree = ""; }; - 0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderRow.swift; sourceTree = ""; }; 0EDE02C127F61C79000FBE3C /* EditableTextList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextList.swift; sourceTree = ""; }; 0EDE8DBF20C86910004C739C /* PassepartoutOpenVPNTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PassepartoutOpenVPNTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0EDE8DC320C86910004C739C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -339,7 +338,6 @@ 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = ""; }; 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = ""; }; 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = ""; }; - 0EF708312811CC8400A3A308 /* VPNStatusText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusText.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -450,7 +448,7 @@ 0EF0FAF527DD0211007EB181 /* PaywallView.swift */, 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */, 0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */, - 0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */, + 0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */, 0E44689527B051C300A14CE4 /* ProfileView.swift */, 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */, 0E92D7F327F104B80033CB7B /* ProfileView+Diagnostics.swift */, @@ -465,7 +463,6 @@ 0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */, 0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */, 0E71ACFA27C12E5300F85C4B /* VersionView.swift */, - 0EF708312811CC8400A3A308 /* VPNStatusText.swift */, 0E7577DE2817E22C00081CBE /* VPNToggle.swift */, 0E065F102813269500062CAF /* WelcomeView.swift */, ); @@ -898,7 +895,6 @@ 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */, 0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */, 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */, - 0EF708322811CC8400A3A308 /* VPNStatusText.swift in Sources */, 0E5324A627D297BB002565C3 /* InApp.swift in Sources */, 0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */, 0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */, @@ -926,6 +922,7 @@ 0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */, 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */, 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */, + 0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */, 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */, 0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */, 0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */, @@ -946,7 +943,6 @@ 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */, 0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */, 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */, - 0ED89C2527DE45A3008B36D6 /* ProfileHeaderRow.swift in Sources */, 0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */, 0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */, 0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */, diff --git a/Passepartout/App/Constants/Theme.swift b/Passepartout/App/Constants/Theme.swift index c9e86e4e..be0c3c46 100644 --- a/Passepartout/App/Constants/Theme.swift +++ b/Passepartout/App/Constants/Theme.swift @@ -42,15 +42,18 @@ extension View { } } -// MARK: Styles +// MARK: Global extension View { func themeGlobal() -> some View { + #if targetEnvironment(macCatalyst) + self + #else let color = themeAccentColor return accentColor(color) .toggleStyle(SwitchToggleStyle(tint: color)) - .listStyle(.insetGrouped) .themeNavigationViewStyle() + #endif } @ViewBuilder @@ -65,11 +68,20 @@ extension View { } func themePrimaryView() -> some View { + #if targetEnvironment(macCatalyst) + navigationBarHidden(true) + #else navigationBarTitleDisplayMode(.large) + #endif } func themeSecondaryView() -> some View { + #if targetEnvironment(macCatalyst) + navigationBarHidden(true) + #else navigationBarTitleDisplayMode(.inline) + .listStyle(.insetGrouped) + #endif } } diff --git a/Passepartout/App/L10n/TunnelKit+L10n.swift b/Passepartout/App/L10n/TunnelKit+L10n.swift index 81fa83c3..53d64f41 100644 --- a/Passepartout/App/L10n/TunnelKit+L10n.swift +++ b/Passepartout/App/L10n/TunnelKit+L10n.swift @@ -52,7 +52,7 @@ extension DataCount { var localizedDescription: String { let down = received.descriptionAsDataUnit let up = sent.descriptionAsDataUnit - return "↓\(down) / ↑\(up)" + return "↓\(down) ↑\(up)" } } diff --git a/Passepartout/App/PassepartoutApp.swift b/Passepartout/App/PassepartoutApp.swift index bc56907c..58aca2b8 100644 --- a/Passepartout/App/PassepartoutApp.swift +++ b/Passepartout/App/PassepartoutApp.swift @@ -31,6 +31,7 @@ struct PassepartoutApp: App { @SceneBuilder var body: some Scene { WindowGroup { MainView() + .withoutTitleBar() .onIntentActivity(IntentDispatcher.connectVPN) .onIntentActivity(IntentDispatcher.disableVPN) .onIntentActivity(IntentDispatcher.enableVPN) diff --git a/Passepartout/App/Reusable/View+Extensions.swift b/Passepartout/App/Reusable/View+Extensions.swift index 17c78674..4ef6cb77 100644 --- a/Passepartout/App/Reusable/View+Extensions.swift +++ b/Passepartout/App/Reusable/View+Extensions.swift @@ -28,6 +28,20 @@ import PassepartoutCore import SwiftyBeaver extension View { + func withoutTitleBar() -> some View { + #if targetEnvironment(macCatalyst) + withHostingWindow { window in + guard let titlebar = window?.windowScene?.titlebar else { + return + } + titlebar.titleVisibility = .hidden + titlebar.toolbar = nil + } + #else + self + #endif + } + func withLeadingText(_ text: String?, color: Color? = nil, truncationMode: Text.TruncationMode = .tail) -> some View { HStack { text.map(Text.init) @@ -121,3 +135,26 @@ extension ScrollViewProxy { } } } + +// https://stackoverflow.com/questions/65238068/hide-title-bar-in-swiftui-app-for-maccatalyst + +private extension View { + func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View { + background(HostingWindowFinder(callback: callback)) + } +} + +private struct HostingWindowFinder: UIViewRepresentable { + var callback: (UIWindow?) -> () + + func makeUIView(context: Context) -> UIView { + let view = UIView() + DispatchQueue.main.async { [weak view] in + self.callback(view?.window) + } + return view + } + + func updateUIView(_ uiView: UIView, context: Context) { + } +} diff --git a/Passepartout/App/Views/OrganizerView+Profiles.swift b/Passepartout/App/Views/OrganizerView+Profiles.swift index d56c6612..fba7ba1b 100644 --- a/Passepartout/App/Views/OrganizerView+Profiles.swift +++ b/Passepartout/App/Views/OrganizerView+Profiles.swift @@ -38,8 +38,27 @@ extension OrganizerView { @Binding private var alertType: AlertType? @State private var isFirstLaunch = true + + @State private var presentedProfileId: UUID? - @State private var isPresentingProfile = false + private var presentedAndLoadedProfileId: Binding { + .init { + presentedProfileId + } set: { + guard let id = $0 else { + presentedProfileId = nil + return + } + presentedProfileId = id + + // load profile contextually with navigation + do { + try profileManager.loadCurrentProfile(withId: id) + } catch { + pp_log.error("Unable to load profile: \(error)") + } + } + } init(alertType: Binding) { profileManager = .shared @@ -50,8 +69,7 @@ extension OrganizerView { var body: some View { debugChanges() - return ZStack { - hiddenProfileLink + return Group { mainView if profileManager.headers.isEmpty { emptyView @@ -64,28 +82,17 @@ extension OrganizerView { // from AddProfileView .onReceive(profileManager.didCreateProfile) { - presentProfile(withId: $0.id) + presentedAndLoadedProfileId.wrappedValue = $0.id } } private var mainView: some View { List { - activeHeaders.map { headers in - Section( - header: Text(L10n.Organizer.Sections.active) - ) { - ForEach(headers, content: profileButton(forHeader:)) - .onDelete(perform: removeActiveProfile) - } - } - let headers = otherHeaders - if !headers.isEmpty { - Section( - header: Text(L10n.Global.Strings.profiles) - ) { - ForEach(headers, content: profileButton(forHeader:)) - .onDelete(perform: removeOtherProfiles) - } + Section( + header: Text(L10n.Global.Strings.profiles) + ) { + ForEach(sortedHeaders, content: profileRow(forHeader:)) + .onDelete(perform: removeProfiles) } }.themeAnimation(on: profileManager.headers) } @@ -97,84 +104,84 @@ extension OrganizerView { } } - private func profileButton(forHeader header: Profile.Header) -> some View { - Button { - presentProfile(withId: header.id) + private func profileRow(forHeader header: Profile.Header) -> some View { + NavigationLink(tag: header.id, selection: presentedAndLoadedProfileId) { + ProfileView() } label: { - ProfileHeaderRow( - header: header, - isActive: profileManager.isActiveProfile(header.id) - ) + profileLabel(forHeader: header) }.contextMenu { - ProfileView.DuplicateButton( - header: header, - switchCurrentProfile: false - ) - }.themeTextButtonStyle() + profileMenu(forHeader: header) + }.onAppear { + presentIfActiveProfile(header.id) + } } - private var hiddenProfileLink: some View { - NavigationLink("", isActive: $isPresentingProfile) { - ProfileView() - }.onAppear(perform: presentActiveProfile) + private func profileLabel(forHeader header: Profile.Header) -> some View { + ProfileRow( + header: header, + isActive: profileManager.isActiveProfile(header.id) + ) + } + + @ViewBuilder + private func profileMenu(forHeader header: Profile.Header) -> some View { + ProfileView.DuplicateButton( + header: header, + switchCurrentProfile: false + ) + } + + private var sortedHeaders: [Profile.Header] { + profileManager.headers + .sorted() + + // FIXME: layout, moving active profile on top breaks row animation (content flashes on Mac) +// .sorted { +// if profileManager.isActiveProfile($0.id) { +// return true +// } else if profileManager.isActiveProfile($1.id) { +// return false +// } else { +// return $0 < $1 +// } +// } } } } extension OrganizerView.ProfilesList { - private var activeHeaders: [Profile.Header]? { - guard let activeHeader = profileManager.activeHeader else { - return nil + private func presentIfActiveProfile(_ id: UUID) { + guard id == profileManager.activeHeader?.id else { + return } - return [activeHeader] - } - - private var otherHeaders: [Profile.Header] { - profileManager.headers - .filter { - !profileManager.isActiveProfile($0.id) - }.sorted() + presentActiveProfile() } private func presentActiveProfile() { - - // do not present profile if: - // - // - an alert is active, as it would break navigation - // - on iPad, as it's already shown - // - guard alertType == nil, themeIdiom != .pad else { - return - } - - guard isFirstLaunch, profileManager.hasActiveProfile else { + guard isFirstLaunch else { return } isFirstLaunch = false - isPresentingProfile = true - } - - private func presentProfile(withId id: UUID) { - isPresentingProfile = true - do { - try profileManager.loadCurrentProfile(withId: id) - } catch { - pp_log.error("Unable to load profile: \(error)") - } - } - private func removeActiveProfile(_ indexSet: IndexSet) { - guard let activeHeader = activeHeaders?.first else { - assertionFailure("Removing active profile while nil?") + // presenting profile when an alert is active seems to break navigation + guard alertType == nil else { return } - removeProfiles(withIds: [activeHeader.id]) + guard let activeProfileId = profileManager.activeHeader?.id else { + return + } + + // FIXME: layout, preselecting profile on iPad portrait/compact adds ProfileView() twice + // can notice becase "Back" needs to be tapped twice to show sidebar + if themeIdiom != .pad { + presentedProfileId = activeProfileId + } } - private func removeOtherProfiles(_ indexSet: IndexSet) { - let currentHeaders = otherHeaders + private func removeProfiles(at offsets: IndexSet) { + let currentHeaders = sortedHeaders var toDelete: [UUID] = [] - indexSet.forEach { + offsets.forEach { toDelete.append(currentHeaders[$0].id) } removeProfiles(withIds: toDelete) @@ -184,7 +191,7 @@ extension OrganizerView.ProfilesList { // clear selection before removal to avoid triggering a bogus navigation push if toDelete.contains(profileManager.currentProfile.value.id) { - isPresentingProfile = false + presentedProfileId = nil } profileManager.removeProfiles(withIds: toDelete) @@ -197,8 +204,8 @@ extension OrganizerView.ProfilesList { } private func dismissSelectionIfDeleted(headers: [Profile.Header]) { - if isPresentingProfile, !profileManager.isCurrentProfileExisting() { - isPresentingProfile = false + if let _ = presentedProfileId, !profileManager.isCurrentProfileExisting() { + presentedProfileId = nil } } } diff --git a/Passepartout/App/Views/ProfileHeaderRow.swift b/Passepartout/App/Views/ProfileHeaderRow.swift deleted file mode 100644 index 5dd7dd20..00000000 --- a/Passepartout/App/Views/ProfileHeaderRow.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// ProfileHeaderRow.swift -// Passepartout -// -// Created by Davide De Rosa on 3/13/22. -// Copyright (c) 2022 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 . -// - -import SwiftUI -import PassepartoutCore - -struct ProfileHeaderRow: View { - let header: Profile.Header - - let isActive: Bool - - var body: some View { - VStack(alignment: .leading, spacing: 5) { - Group { - if let name = header.providerName { - providerView(name) - } else { - hostView - } - }.themeLongTextStyle() - .font(isActive ? .headline : .body) - - if isActive { - VPNStatusText() - .themeSecondaryTextStyle() - .font(.subheadline) - } - }.frame(height: 60) - } - - private func providerView(_ name: ProviderName) -> some View { -// Label(header.name, systemImage: themeProviderImage) -// Label(header.name, image: themeAssetsProviderImage(name)) - Text(header.name) - } - - private var hostView: some View { -// Label(header.name, systemImage: themeHostImage) - Text(header.name) - } -} diff --git a/Passepartout/App/Views/ProfileRow.swift b/Passepartout/App/Views/ProfileRow.swift new file mode 100644 index 00000000..f9527ebc --- /dev/null +++ b/Passepartout/App/Views/ProfileRow.swift @@ -0,0 +1,87 @@ +// +// ProfileRow.swift +// Passepartout +// +// Created by Davide De Rosa on 4/28/22. +// Copyright (c) 2022 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 . +// + +import SwiftUI +import PassepartoutCore + +struct ProfileRow: View { + let header: Profile.Header + + let isActive: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + nameView + .font(.headline) + .themeLongTextStyle() + + VPNStateView(isActive: isActive) + .font(.subheadline) + .themeSecondaryTextStyle() + }.padding([.top, .bottom], 10) + } + + private var nameView: some View { + Text(header.name) + } + + struct VPNStateView: View { + @ObservedObject private var currentVPNState: VPNManager.ObservableState + + private let isActive: Bool + + init(isActive: Bool) { + currentVPNState = .shared + self.isActive = isActive + } + + var body: some View { + HStack { +// Image(systemName: isActive ? "dot.radiowaves.up.forward" : "circle") + if isActive { + Image(systemName: "circle.fill") + Text(statusDescription) + currentVPNState.dataCount.map { + Text($0.localizedDescription) + } + } else { + Image(systemName: "circle") + Text(L10n.Tunnelkit.Vpn.unused) + } + } + } + + private var statusDescription: String { + if currentVPNState.vpnStatus != .disconnected { + return currentVPNState.localizedStatusDescription( + withErrors: false, + dataCountIfAvailable: false + ) + } else { + return L10n.Organizer.Sections.active + } + } + } +} diff --git a/Passepartout/App/Views/VPNStatusText.swift b/Passepartout/App/Views/VPNStatusText.swift deleted file mode 100644 index 2514323a..00000000 --- a/Passepartout/App/Views/VPNStatusText.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// VPNStatusText.swift -// Passepartout -// -// Created by Davide De Rosa on 4/21/22. -// Copyright (c) 2022 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 . -// - -import SwiftUI -import PassepartoutCore - -struct VPNStatusText: View { - @ObservedObject private var currentVPNState: VPNManager.ObservableState - - init() { - currentVPNState = .shared - } - - var body: some View { - debugChanges() - return HStack { - Text(statusDescription) - Spacer() - currentVPNState.dataCount.map { - Text($0.localizedDescription) - } - } - } - - private var statusDescription: String { - currentVPNState.localizedStatusDescription( - withErrors: false, - dataCountIfAvailable: false - ) - } -}