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.
This commit is contained in:
Davide De Rosa 2022-04-27 16:13:01 +02:00
parent 4d13d8bf6b
commit 0047d095fb
9 changed files with 230 additions and 206 deletions

View File

@ -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 = "<group>"; };
0EBE2FD72360F89600F0D5AB /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0EBE2FD82360F89600F0D5AB /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRow.swift; sourceTree = "<group>"; };
0ECF71ED27B6A99300CDB528 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
0ED1D6DB27DBA41700983466 /* DiagnosticsView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiagnosticsView+OpenVPN.swift"; sourceTree = "<group>"; };
0ED1D6DD27DBA42100983466 /* DiagnosticsView+WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiagnosticsView+WireGuard.swift"; sourceTree = "<group>"; };
@ -325,7 +325,6 @@
0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentEditView.swift; sourceTree = "<group>"; };
0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShortcutsView+Add.swift"; sourceTree = "<group>"; };
0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentAddView.swift; sourceTree = "<group>"; };
0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderRow.swift; sourceTree = "<group>"; };
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextList.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@ -339,7 +338,6 @@
0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = "<group>"; };
0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = "<group>"; };
0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = "<group>"; };
0EF708312811CC8400A3A308 /* VPNStatusText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusText.swift; sourceTree = "<group>"; };
/* 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 */,

View File

@ -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
}
}

View File

@ -52,7 +52,7 @@ extension DataCount {
var localizedDescription: String {
let down = received.descriptionAsDataUnit
let up = sent.descriptionAsDataUnit
return "\(down) / \(up)"
return "\(down) \(up)"
}
}

View File

@ -31,6 +31,7 @@ struct PassepartoutApp: App {
@SceneBuilder var body: some Scene {
WindowGroup {
MainView()
.withoutTitleBar()
.onIntentActivity(IntentDispatcher.connectVPN)
.onIntentActivity(IntentDispatcher.disableVPN)
.onIntentActivity(IntentDispatcher.enableVPN)

View File

@ -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) {
}
}

View File

@ -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<UUID?> {
.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<AlertType?>) {
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
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
)
}
}