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:
parent
4d13d8bf6b
commit
0047d095fb
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ extension DataCount {
|
|||
var localizedDescription: String {
|
||||
let down = received.descriptionAsDataUnit
|
||||
let up = sent.descriptionAsDataUnit
|
||||
return "↓\(down) / ↑\(up)"
|
||||
return "↓\(down) ↑\(up)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ struct PassepartoutApp: App {
|
|||
@SceneBuilder var body: some Scene {
|
||||
WindowGroup {
|
||||
MainView()
|
||||
.withoutTitleBar()
|
||||
.onIntentActivity(IntentDispatcher.connectVPN)
|
||||
.onIntentActivity(IntentDispatcher.disableVPN)
|
||||
.onIntentActivity(IntentDispatcher.enableVPN)
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,26 @@ extension OrganizerView {
|
|||
|
||||
@State private var isFirstLaunch = true
|
||||
|
||||
@State private var isPresentingProfile = false
|
||||
@State private var presentedProfileId: UUID?
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue