mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-02-16 12:52:11 +00:00
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 */; };
|
0EBC075B27EC4FFF00208AD9 /* ReportIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */; };
|
||||||
0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075C27EC529000208AD9 /* DebugLog+Constants.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 */; };
|
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 */; };
|
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF71ED27B6A99300CDB528 /* AccountView.swift */; };
|
||||||
0ED1D6DC27DBA41700983466 /* DiagnosticsView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED1D6DB27DBA41700983466 /* DiagnosticsView+OpenVPN.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 */; };
|
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 */; };
|
0ED89C1727DE0E05008B36D6 /* IntentEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */; };
|
||||||
0ED89C1C27DE3ABC008B36D6 /* ShortcutsView+Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.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 */; };
|
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 */; };
|
0EDE02C227F61C79000FBE3C /* EditableTextList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE02C127F61C79000FBE3C /* EditableTextList.swift */; };
|
||||||
0EE11CD2280D8317003BE431 /* OrganizerView+SettingsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE11CD1280D8317003BE431 /* OrganizerView+SettingsMenu.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 */; };
|
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 */; };
|
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; };
|
||||||
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; };
|
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; };
|
||||||
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -450,7 +448,7 @@
|
|||||||
0EF0FAF527DD0211007EB181 /* PaywallView.swift */,
|
0EF0FAF527DD0211007EB181 /* PaywallView.swift */,
|
||||||
0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */,
|
0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */,
|
||||||
0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */,
|
0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */,
|
||||||
0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */,
|
0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */,
|
||||||
0E44689527B051C300A14CE4 /* ProfileView.swift */,
|
0E44689527B051C300A14CE4 /* ProfileView.swift */,
|
||||||
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
|
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
|
||||||
0E92D7F327F104B80033CB7B /* ProfileView+Diagnostics.swift */,
|
0E92D7F327F104B80033CB7B /* ProfileView+Diagnostics.swift */,
|
||||||
@ -465,7 +463,6 @@
|
|||||||
0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */,
|
0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */,
|
||||||
0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */,
|
0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */,
|
||||||
0E71ACFA27C12E5300F85C4B /* VersionView.swift */,
|
0E71ACFA27C12E5300F85C4B /* VersionView.swift */,
|
||||||
0EF708312811CC8400A3A308 /* VPNStatusText.swift */,
|
|
||||||
0E7577DE2817E22C00081CBE /* VPNToggle.swift */,
|
0E7577DE2817E22C00081CBE /* VPNToggle.swift */,
|
||||||
0E065F102813269500062CAF /* WelcomeView.swift */,
|
0E065F102813269500062CAF /* WelcomeView.swift */,
|
||||||
);
|
);
|
||||||
@ -898,7 +895,6 @@
|
|||||||
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */,
|
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */,
|
||||||
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */,
|
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */,
|
||||||
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
|
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
|
||||||
0EF708322811CC8400A3A308 /* VPNStatusText.swift in Sources */,
|
|
||||||
0E5324A627D297BB002565C3 /* InApp.swift in Sources */,
|
0E5324A627D297BB002565C3 /* InApp.swift in Sources */,
|
||||||
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
|
||||||
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
|
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
|
||||||
@ -926,6 +922,7 @@
|
|||||||
0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */,
|
0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */,
|
||||||
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */,
|
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */,
|
||||||
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
|
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
|
||||||
|
0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */,
|
||||||
0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */,
|
0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */,
|
||||||
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */,
|
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */,
|
||||||
0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */,
|
0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */,
|
||||||
@ -946,7 +943,6 @@
|
|||||||
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */,
|
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */,
|
||||||
0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */,
|
0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */,
|
||||||
0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */,
|
0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */,
|
||||||
0ED89C2527DE45A3008B36D6 /* ProfileHeaderRow.swift in Sources */,
|
|
||||||
0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */,
|
0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */,
|
||||||
0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */,
|
0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */,
|
||||||
0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */,
|
0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */,
|
||||||
|
@ -42,15 +42,18 @@ extension View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Styles
|
// MARK: Global
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func themeGlobal() -> some View {
|
func themeGlobal() -> some View {
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
self
|
||||||
|
#else
|
||||||
let color = themeAccentColor
|
let color = themeAccentColor
|
||||||
return accentColor(color)
|
return accentColor(color)
|
||||||
.toggleStyle(SwitchToggleStyle(tint: color))
|
.toggleStyle(SwitchToggleStyle(tint: color))
|
||||||
.listStyle(.insetGrouped)
|
|
||||||
.themeNavigationViewStyle()
|
.themeNavigationViewStyle()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -65,11 +68,20 @@ extension View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func themePrimaryView() -> some View {
|
func themePrimaryView() -> some View {
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
navigationBarHidden(true)
|
||||||
|
#else
|
||||||
navigationBarTitleDisplayMode(.large)
|
navigationBarTitleDisplayMode(.large)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeSecondaryView() -> some View {
|
func themeSecondaryView() -> some View {
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
navigationBarHidden(true)
|
||||||
|
#else
|
||||||
navigationBarTitleDisplayMode(.inline)
|
navigationBarTitleDisplayMode(.inline)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ extension DataCount {
|
|||||||
var localizedDescription: String {
|
var localizedDescription: String {
|
||||||
let down = received.descriptionAsDataUnit
|
let down = received.descriptionAsDataUnit
|
||||||
let up = sent.descriptionAsDataUnit
|
let up = sent.descriptionAsDataUnit
|
||||||
return "↓\(down) / ↑\(up)"
|
return "↓\(down) ↑\(up)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ struct PassepartoutApp: App {
|
|||||||
@SceneBuilder var body: some Scene {
|
@SceneBuilder var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainView()
|
MainView()
|
||||||
|
.withoutTitleBar()
|
||||||
.onIntentActivity(IntentDispatcher.connectVPN)
|
.onIntentActivity(IntentDispatcher.connectVPN)
|
||||||
.onIntentActivity(IntentDispatcher.disableVPN)
|
.onIntentActivity(IntentDispatcher.disableVPN)
|
||||||
.onIntentActivity(IntentDispatcher.enableVPN)
|
.onIntentActivity(IntentDispatcher.enableVPN)
|
||||||
|
@ -28,6 +28,20 @@ import PassepartoutCore
|
|||||||
import SwiftyBeaver
|
import SwiftyBeaver
|
||||||
|
|
||||||
extension View {
|
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 {
|
func withLeadingText(_ text: String?, color: Color? = nil, truncationMode: Text.TruncationMode = .tail) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
text.map(Text.init)
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -38,8 +38,27 @@ extension OrganizerView {
|
|||||||
@Binding private var alertType: AlertType?
|
@Binding private var alertType: AlertType?
|
||||||
|
|
||||||
@State private var isFirstLaunch = true
|
@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?>) {
|
init(alertType: Binding<AlertType?>) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
@ -50,8 +69,7 @@ extension OrganizerView {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return ZStack {
|
return Group {
|
||||||
hiddenProfileLink
|
|
||||||
mainView
|
mainView
|
||||||
if profileManager.headers.isEmpty {
|
if profileManager.headers.isEmpty {
|
||||||
emptyView
|
emptyView
|
||||||
@ -64,28 +82,17 @@ extension OrganizerView {
|
|||||||
|
|
||||||
// from AddProfileView
|
// from AddProfileView
|
||||||
.onReceive(profileManager.didCreateProfile) {
|
.onReceive(profileManager.didCreateProfile) {
|
||||||
presentProfile(withId: $0.id)
|
presentedAndLoadedProfileId.wrappedValue = $0.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
List {
|
List {
|
||||||
activeHeaders.map { headers in
|
Section(
|
||||||
Section(
|
header: Text(L10n.Global.Strings.profiles)
|
||||||
header: Text(L10n.Organizer.Sections.active)
|
) {
|
||||||
) {
|
ForEach(sortedHeaders, content: profileRow(forHeader:))
|
||||||
ForEach(headers, content: profileButton(forHeader:))
|
.onDelete(perform: removeProfiles)
|
||||||
.onDelete(perform: removeActiveProfile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let headers = otherHeaders
|
|
||||||
if !headers.isEmpty {
|
|
||||||
Section(
|
|
||||||
header: Text(L10n.Global.Strings.profiles)
|
|
||||||
) {
|
|
||||||
ForEach(headers, content: profileButton(forHeader:))
|
|
||||||
.onDelete(perform: removeOtherProfiles)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.themeAnimation(on: profileManager.headers)
|
}.themeAnimation(on: profileManager.headers)
|
||||||
}
|
}
|
||||||
@ -97,84 +104,84 @@ extension OrganizerView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func profileButton(forHeader header: Profile.Header) -> some View {
|
private func profileRow(forHeader header: Profile.Header) -> some View {
|
||||||
Button {
|
NavigationLink(tag: header.id, selection: presentedAndLoadedProfileId) {
|
||||||
presentProfile(withId: header.id)
|
ProfileView()
|
||||||
} label: {
|
} label: {
|
||||||
ProfileHeaderRow(
|
profileLabel(forHeader: header)
|
||||||
header: header,
|
|
||||||
isActive: profileManager.isActiveProfile(header.id)
|
|
||||||
)
|
|
||||||
}.contextMenu {
|
}.contextMenu {
|
||||||
ProfileView.DuplicateButton(
|
profileMenu(forHeader: header)
|
||||||
header: header,
|
}.onAppear {
|
||||||
switchCurrentProfile: false
|
presentIfActiveProfile(header.id)
|
||||||
)
|
}
|
||||||
}.themeTextButtonStyle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hiddenProfileLink: some View {
|
private func profileLabel(forHeader header: Profile.Header) -> some View {
|
||||||
NavigationLink("", isActive: $isPresentingProfile) {
|
ProfileRow(
|
||||||
ProfileView()
|
header: header,
|
||||||
}.onAppear(perform: presentActiveProfile)
|
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 {
|
extension OrganizerView.ProfilesList {
|
||||||
private var activeHeaders: [Profile.Header]? {
|
private func presentIfActiveProfile(_ id: UUID) {
|
||||||
guard let activeHeader = profileManager.activeHeader else {
|
guard id == profileManager.activeHeader?.id else {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
return [activeHeader]
|
presentActiveProfile()
|
||||||
}
|
|
||||||
|
|
||||||
private var otherHeaders: [Profile.Header] {
|
|
||||||
profileManager.headers
|
|
||||||
.filter {
|
|
||||||
!profileManager.isActiveProfile($0.id)
|
|
||||||
}.sorted()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentActiveProfile() {
|
private func presentActiveProfile() {
|
||||||
|
guard isFirstLaunch else {
|
||||||
// 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 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isFirstLaunch = false
|
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) {
|
// presenting profile when an alert is active seems to break navigation
|
||||||
guard let activeHeader = activeHeaders?.first else {
|
guard alertType == nil else {
|
||||||
assertionFailure("Removing active profile while nil?")
|
|
||||||
return
|
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) {
|
private func removeProfiles(at offsets: IndexSet) {
|
||||||
let currentHeaders = otherHeaders
|
let currentHeaders = sortedHeaders
|
||||||
var toDelete: [UUID] = []
|
var toDelete: [UUID] = []
|
||||||
indexSet.forEach {
|
offsets.forEach {
|
||||||
toDelete.append(currentHeaders[$0].id)
|
toDelete.append(currentHeaders[$0].id)
|
||||||
}
|
}
|
||||||
removeProfiles(withIds: toDelete)
|
removeProfiles(withIds: toDelete)
|
||||||
@ -184,7 +191,7 @@ extension OrganizerView.ProfilesList {
|
|||||||
|
|
||||||
// clear selection before removal to avoid triggering a bogus navigation push
|
// clear selection before removal to avoid triggering a bogus navigation push
|
||||||
if toDelete.contains(profileManager.currentProfile.value.id) {
|
if toDelete.contains(profileManager.currentProfile.value.id) {
|
||||||
isPresentingProfile = false
|
presentedProfileId = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
profileManager.removeProfiles(withIds: toDelete)
|
profileManager.removeProfiles(withIds: toDelete)
|
||||||
@ -197,8 +204,8 @@ extension OrganizerView.ProfilesList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
|
private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
|
||||||
if isPresentingProfile, !profileManager.isCurrentProfileExisting() {
|
if let _ = presentedProfileId, !profileManager.isCurrentProfileExisting() {
|
||||||
isPresentingProfile = false
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
87
Passepartout/App/Views/ProfileRow.swift
Normal file
87
Passepartout/App/Views/ProfileRow.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
Block a user