Group Organizer modals into toolbar menus

- Drop status / navigation bars colors

- Restore large title on iPad

- Overlay organizer with "No profiles" when empty

- Uninstall VPN from ProfileView
This commit is contained in:
Davide De Rosa 2022-04-18 12:01:42 +02:00
parent 6533a6beae
commit 18161ed1f1
17 changed files with 363 additions and 410 deletions

View File

@ -24,15 +24,13 @@
0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */; };
0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */; };
0E34A2CF27CADA6300C73B67 /* GenericVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */; };
0E34AC7627F83FE20042F2AB /* OrganizerView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7527F83FE20042F2AB /* OrganizerView+VPN.swift */; };
0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */; };
0E34AC7A27F8431D0042F2AB /* OrganizerView+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7927F8431D0042F2AB /* OrganizerView+Shortcuts.swift */; };
0E34AC7C27F845510042F2AB /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */; };
0E34AC7E27F849050042F2AB /* OrganizerView+AddProfileMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7D27F849050042F2AB /* OrganizerView+AddProfileMenu.swift */; };
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */; };
0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FCC27E47B3700C66F13 /* AddHostView.swift */; };
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */; };
0E3B7FDA27E51A0200C66F13 /* ProfileView+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */; };
0E3CD47F280DA14B007075C0 /* OrganizerView+AddMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */; };
0E44689627B051C300A14CE4 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E44689527B051C300A14CE4 /* ProfileView.swift */; };
0E44689C27B11B5300A14CE4 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E44689B27B11B5300A14CE4 /* AboutView.swift */; };
0E49F6BB27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49F6BA27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift */; };
@ -110,6 +108,7 @@
0ED89C2027DE423B008B36D6 /* ShortcutsView+ConnectTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1F27DE423B008B36D6 /* ShortcutsView+ConnectTo.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 */; };
0EED0BB92733CEDA00C9FC68 /* PassepartoutCore in Frameworks */ = {isa = PBXBuildFile; productRef = 0EED0BB82733CEDA00C9FC68 /* PassepartoutCore */; };
0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0FAF527DD0211007EB181 /* PaywallView.swift */; };
@ -205,15 +204,13 @@
0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenVPN+L10n.swift"; sourceTree = "<group>"; };
0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Core+L10n.swift"; sourceTree = "<group>"; };
0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericVersionView.swift; sourceTree = "<group>"; };
0E34AC7527F83FE20042F2AB /* OrganizerView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+VPN.swift"; sourceTree = "<group>"; };
0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Scene.swift"; sourceTree = "<group>"; };
0E34AC7927F8431D0042F2AB /* OrganizerView+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Shortcuts.swift"; sourceTree = "<group>"; };
0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = "<group>"; };
0E34AC7D27F849050042F2AB /* OrganizerView+AddProfileMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+AddProfileMenu.swift"; sourceTree = "<group>"; };
0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnDemandView+SSID.swift"; sourceTree = "<group>"; };
0E3B7FCC27E47B3700C66F13 /* AddHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostView.swift; sourceTree = "<group>"; };
0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+VPN.swift"; sourceTree = "<group>"; };
0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Provider.swift"; sourceTree = "<group>"; };
0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+AddMenu.swift"; sourceTree = "<group>"; };
0E44689527B051C300A14CE4 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
0E44689B27B11B5300A14CE4 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
0E49F6BA27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointAdvancedView+OpenVPN.swift"; sourceTree = "<group>"; };
@ -325,6 +322,7 @@
0EDE8DC320C86910004C739C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0EDE8DD220C86978004C739C /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; };
0EDE8DE220C86A13004C739C /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
0EE11CD1280D8317003BE431 /* OrganizerView+SettingsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+SettingsMenu.swift"; sourceTree = "<group>"; };
0EE8B7E227FF340F00B68621 /* VPNProtocolType+FileExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNProtocolType+FileExtensions.swift"; sourceTree = "<group>"; };
0EF0FAF527DD0211007EB181 /* PaywallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallView.swift; sourceTree = "<group>"; };
0EF0FAF827DD212C007EB181 /* IntentActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentActivity.swift; sourceTree = "<group>"; };
@ -391,11 +389,10 @@
0EB34BC927C6A70200B126DA /* OnDemandView.swift */,
0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */,
0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */,
0E34AC7D27F849050042F2AB /* OrganizerView+AddProfileMenu.swift */,
0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */,
0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */,
0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */,
0E34AC7927F8431D0042F2AB /* OrganizerView+Shortcuts.swift */,
0E34AC7527F83FE20042F2AB /* OrganizerView+VPN.swift */,
0EE11CD1280D8317003BE431 /* OrganizerView+SettingsMenu.swift */,
0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */,
0E44689527B051C300A14CE4 /* ProfileView.swift */,
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
@ -937,8 +934,6 @@
0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */,
0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */,
0E92D7C627F103300033CB7B /* ProfileView+Configuration.swift in Sources */,
0E34AC7A27F8431D0042F2AB /* OrganizerView+Shortcuts.swift in Sources */,
0E34AC7E27F849050042F2AB /* OrganizerView+AddProfileMenu.swift in Sources */,
0E2DE71C27DCCFE80067B9E1 /* TunnelKit+Identifiable.swift in Sources */,
0ED1D6DE27DBA42100983466 /* DiagnosticsView+WireGuard.swift in Sources */,
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */,
@ -964,6 +959,7 @@
0E34A2CF27CADA6300C73B67 /* GenericVersionView.swift in Sources */,
0E9C233327F47E95007D5FC7 /* IntentDispatcher+Activities.swift in Sources */,
0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */,
0E3CD47F280DA14B007075C0 /* OrganizerView+AddMenu.swift in Sources */,
0EB17EAA27D226C900D473B5 /* Constants+Extensions.swift in Sources */,
0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */,
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */,
@ -986,7 +982,6 @@
0E49F6BB27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift in Sources */,
0E71ACEF27C106B500F85C4B /* ProviderPresetView.swift in Sources */,
0E0AD49027BD53CB00FBB520 /* ProfileView+Welcome.swift in Sources */,
0E34AC7627F83FE20042F2AB /* OrganizerView+VPN.swift in Sources */,
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */,
0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */,
0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */,
@ -1001,6 +996,7 @@
0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */,
0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */,
0E9ED48127FD9BAE003B2316 /* CopySavingButton.swift in Sources */,
0EE11CD2280D8317003BE431 /* OrganizerView+SettingsMenu.swift in Sources */,
0E44689C27B11B5300A14CE4 /* AboutView.swift in Sources */,
0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */,
0ED89C1527DE0A0C008B36D6 /* Shortcut.swift in Sources */,

View File

@ -37,28 +37,6 @@ extension Color {
}
extension View {
@available(iOS 14, *)
func themeConfigureNavigationBarAppearance() {
let navBackgroundColor = Asset.Assets.primaryColor.color
let titleAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: Asset.Assets.lightTextColor.color
]
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithOpaqueBackground()
navBarAppearance.backgroundColor = navBackgroundColor
navBarAppearance.titleTextAttributes = titleAttributes
navBarAppearance.largeTitleTextAttributes = titleAttributes
let navBar = UINavigationBar.appearance()
navBar.standardAppearance = navBarAppearance
navBar.compactAppearance = navBarAppearance
navBar.scrollEdgeAppearance = navBarAppearance
// UITableView.appearance().backgroundColor = .clear
}
@available(iOS 14, *)
var themeIdiom: UIUserInterfaceIdiom {
UIDevice.current.userInterfaceIdiom
@ -78,7 +56,7 @@ extension View {
@ViewBuilder
private func themeNavigationViewStyle() -> some View {
switch UIDevice.current.userInterfaceIdiom {
switch themeIdiom {
case .phone:
navigationViewStyle(.stack)
@ -87,15 +65,8 @@ extension View {
}
}
@ViewBuilder
func themePrimaryView() -> some View {
switch UIDevice.current.userInterfaceIdiom {
case .phone:
navigationBarTitleDisplayMode(.large)
default:
themeSecondaryView()
}
navigationBarTitleDisplayMode(.large)
}
func themeSecondaryView() -> some View {
@ -106,6 +77,11 @@ extension View {
lineLimit(1)
.truncationMode(.middle)
}
func themeInformativeText() -> some View {
font(.title)
.foregroundColor(themeSecondaryColor)
}
}
// MARK: Colors
@ -180,10 +156,14 @@ extension View {
"externaldrive.connected.to.line.below.fill"
}
var themeAddProfileImage: String {
var themeSettingsMenuImage: String {
"ellipsis.circle"
}
var themeAddMenuImage: String {
"plus"
}
var themeCheckmarkImage: String {
"checkmark"
}

View File

@ -103,6 +103,10 @@ class ProductManager: NSObject, ObservableObject {
SKPaymentQueue.default().remove(self)
}
func canMakePayments() -> Bool {
SKPaymentQueue.canMakePayments()
}
func refreshProducts() {
let ids = LocalProduct.all
guard !ids.isEmpty else {

View File

@ -78,8 +78,6 @@
<array>
<string>arm64</string>
</array>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@ -93,8 +91,6 @@
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>com.algoritmico.Passepartout.config</key>
<dict>
<key>appstore_id</key>

View File

@ -28,7 +28,7 @@ import TunnelKitManager
import TunnelKitOpenVPN
import TunnelKitWireGuard
import NetworkExtension
import PassepartoutUtils
import PassepartoutCore
extension VPNStatus {
var localizedDescription: String {

View File

@ -26,17 +26,6 @@
import SwiftUI
struct AboutView: View {
enum ModalType: Identifiable {
case share([Any])
// XXX: alert ids
var id: Int {
return 1
}
}
@State private var modalType: ModalType?
private let versionString = Constants.Global.appVersionString
private let readmeURL = Constants.URLs.readme
@ -51,24 +40,13 @@ struct AboutView: View {
private let privacyURL = Constants.URLs.privacyPolicy
private let alternativeToURL = Constants.URLs.alternativeTo
private let shareMessage = L10n.Global.Messages.share
var body: some View {
List {
infoSubview
githubSubview
webSubview
shareSubview
}.themeSecondaryView()
.navigationTitle(L10n.About.title)
.sheet(item: $modalType) {
switch $0 {
case .share(let items):
ActivityView(activityItems: items)
}
}
}
private var infoSubview: some View {
@ -116,28 +94,4 @@ struct AboutView: View {
}
}
}
private var shareSubview: some View {
Section(
header: Text(L10n.About.Sections.Share.header)
) {
Button(L10n.About.Items.ShareTwitter.caption, action: shareOnTwitter)
Button(L10n.About.Items.ShareGeneric.caption, action: shareWithFriend)
Button(Unlocalized.About.alternativeTo, action: shareAlternativeTo)
}
}
private func shareOnTwitter() {
let url = Unlocalized.Social.twitterIntent(withMessage: shareMessage)
URL.openURL(url)
}
private func shareWithFriend() {
let shareMessage = "\(shareMessage) \(Constants.URLs.website)"
modalType = .share([shareMessage])
}
private func shareAlternativeTo() {
URL.openURL(alternativeToURL)
}
}

View File

@ -26,10 +26,6 @@
import SwiftUI
struct MainView: View {
init() {
themeConfigureNavigationBarAppearance()
}
var body: some View {
NavigationView {
OrganizerView()

View File

@ -1,8 +1,8 @@
//
// OrganizerView+AddProfileMenu.swift
// OrganizerView+AddMenu.swift
// Passepartout
//
// Created by Davide De Rosa on 4/2/22.
// Created by Davide De Rosa on 4/18/22.
// Copyright (c) 2022 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@ -27,31 +27,21 @@ import SwiftUI
import PassepartoutCore
extension OrganizerView {
struct AddProfileMenu: View {
struct Bindings {
@Binding var modalType: ModalType?
@Binding var alertType: AlertType?
@Binding var isHostFileImporterPresented: Bool
}
private let withImportedURLs: Bool
private let bindings: Bindings
init(
withImportedURLs: Bool,
bindings: Bindings
) {
self.withImportedURLs = withImportedURLs
self.bindings = bindings
struct AddMenu: View {
@Binding private var modalType: ModalType?
@Binding private var isHostFileImporterPresented: Bool
init(modalType: Binding<ModalType?>, isHostFileImporterPresented: Binding<Bool>) {
_modalType = modalType
_isHostFileImporterPresented = isHostFileImporterPresented
}
// FIXME: l10n, shorten menu captions
var body: some View {
Group {
Menu {
Button {
bindings.modalType = .addProvider
modalType = .addProvider
} label: {
Label(L10n.Organizer.Items.AddProvider.caption, systemImage: themeProviderImage)
}
@ -60,12 +50,12 @@ extension OrganizerView {
} label: {
Label(L10n.Organizer.Items.AddHost.caption, systemImage: themeHostImage)
}
if withImportedURLs {
if let urls = importedURLs, !urls.isEmpty {
Divider()
importedURLs.map { urls in
ForEach(urls, id: \.absoluteString, content: importedURLRow)
}
ForEach(urls, id: \.absoluteString, content: importedURLRow)
}
} label: {
themeAddMenuImage.asSystemImage
}
}
@ -86,33 +76,31 @@ extension OrganizerView {
return nil
}
}
}
}
extension OrganizerView.AddProfileMenu {
private func presentAddProvider() {
bindings.modalType = .addProvider
}
private func presentAddHost(withURL url: URL, deletingURLOnSuccess: Bool) {
bindings.modalType = .addHost(url, deletingURLOnSuccess)
}
private func presentHostFileImporter() {
// XXX: iOS bug, hack around crappy bug when dismissing by swiping down
//
// https://stackoverflow.com/questions/66965471/swiftui-fileimporter-modifier-not-updating-binding-when-dismissed-by-tapping
bindings.isHostFileImporterPresented = false
Task {
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter)
bindings.isHostFileImporterPresented = true
private func presentAddProvider() {
modalType = .addProvider
}
// isHostFileImporterPresented = true
// // use this to test hardcoded bundle file
// let url = Bundle.main.url(forResource: "pia", withExtension: "ovpn")!
// importedProfileName = "pia.ovpn"
// modalType = .addHost(url, false)
private func presentAddHost(withURL url: URL, deletingURLOnSuccess: Bool) {
modalType = .addHost(url, deletingURLOnSuccess)
}
private func presentHostFileImporter() {
// XXX: iOS bug, hack around crappy bug when dismissing by swiping down
//
// https://stackoverflow.com/questions/66965471/swiftui-fileimporter-modifier-not-updating-binding-when-dismissed-by-tapping
isHostFileImporterPresented = false
Task {
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter)
isHostFileImporterPresented = true
}
// isHostFileImporterPresented = true
// // use this to test hardcoded bundle file
// let url = Bundle.main.url(forResource: "pia", withExtension: "ovpn")!
// importedProfileName = "pia.ovpn"
// modalType = .addHost(url, false)
}
}
}

View File

@ -27,7 +27,7 @@ import SwiftUI
import PassepartoutCore
extension OrganizerView {
struct ProfilesSection: View {
struct ProfilesList: View {
@ObservedObject private var appManager: AppManager
@ObservedObject private var profileManager: ProfileManager
@ -36,39 +36,32 @@ extension OrganizerView {
// just to observe changes in profiles eligibility
@ObservedObject private var productManager: ProductManager
private let addProfileMenuBindings: AddProfileMenu.Bindings
@Binding private var alertType: AlertType?
@State private var isFirstLaunch = true
@State private var selectedProfileId: UUID?
init(addProfileMenuBindings: AddProfileMenu.Bindings) {
init(alertType: Binding<AlertType?>) {
appManager = .shared
profileManager = .shared
providerManager = .shared
productManager = .shared
self.addProfileMenuBindings = addProfileMenuBindings
_alertType = alertType
}
var body: some View {
debugChanges()
return Section {
ReloadingContent(
observing: profileManager.headers,
equality: {
Set($0) == Set($1)
}
) {
if !$0.isEmpty {
ForEach($0.sorted(), content: navigationLink(forHeader:))
.onAppear(perform: selectActiveProfile)
} else {
AddProfileMenu(
withImportedURLs: false,
bindings: addProfileMenuBindings
)
}
return ReloadingContent(
observing: profileManager.headers,
equality: {
Set($0) == Set($1)
}
) { headers in
mainView(headers)
if headers.isEmpty {
emptyView
}
}.onAppear(perform: performMigrationsIfNeeded)
@ -80,6 +73,23 @@ extension OrganizerView {
selectedProfileId = $0.id
}
}
private func mainView(_ headers: [Profile.Header]) -> some View {
List {
Section {
ForEach(headers.sorted(), content: navigationLink(forHeader:))
.onAppear(perform: selectActiveProfile)
}
}
}
// FIXME: l10n
private var emptyView: some View {
VStack {
Text("No profiles")
.themeInformativeText()
}
}
private func navigationLink(forHeader header: Profile.Header) -> some View {
NavigationLink(tag: header.id, selection: $selectedProfileId) {
@ -95,7 +105,7 @@ extension OrganizerView {
}
}
extension OrganizerView.ProfilesSection {
extension OrganizerView.ProfilesList {
struct ActiveProfileHeaderRow: View {
@ObservedObject private var currentVPNState: VPNManager.ObservableState
@ -121,7 +131,7 @@ extension OrganizerView.ProfilesSection {
}
}
extension OrganizerView.ProfilesSection {
extension OrganizerView.ProfilesList {
private func selectActiveProfile() {
guard isFirstLaunch else {
return
@ -133,7 +143,7 @@ extension OrganizerView.ProfilesSection {
// - an alert is active, as it would break navigation
// - on iPad, as it's already shown
//
if addProfileMenuBindings.alertType == nil,
if alertType == nil,
themeIdiom != .pad,
let activeProfileId = profileManager.activeHeader?.id {
@ -146,7 +156,7 @@ extension OrganizerView.ProfilesSection {
await appManager.doMigrations(profileManager)
}
}
private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
if let selectedProfileId = selectedProfileId,
!profileManager.isExistingProfile(withId: selectedProfileId) {

View File

@ -0,0 +1,148 @@
//
// OrganizerView+SettingsMenu.swift
// Passepartout
//
// Created by Davide De Rosa on 4/18/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
extension OrganizerView {
struct SettingsMenu: View {
@ObservedObject private var productManager: ProductManager
@Binding var modalType: ModalType?
@Binding var alertType: AlertType?
private var isEligibleForSiri: Bool {
productManager.isEligible(forFeature: .siriShortcuts)
}
private let redditURL = Constants.URLs.subreddit
private let alternativeToURL = Constants.URLs.alternativeTo
private let shareMessage = L10n.Global.Messages.share
private let appName = Unlocalized.appName
init(modalType: Binding<ModalType?>, alertType: Binding<AlertType?>) {
productManager = .shared
_modalType = modalType
_alertType = alertType
}
var body: some View {
Menu {
Menu(L10n.Menu.Support.title) {
supportMenu
}
// FIXME: l10n, refactor string id to "menu.share.title"
Menu(L10n.About.Sections.Share.header) {
shareMenu
}
Divider()
shortcutsButton
aboutButton
// RemoveVPNSection()
// betaSection
} label: {
themeSettingsMenuImage.asSystemImage
}
}
private var shortcutsButton: some View {
Button {
presentShortcutsOrPaywall()
} label: {
Label(L10n.Organizer.Items.SiriShortcuts.caption, systemImage: themeShortcutsImage)
}
}
private var supportMenu: some View {
Group {
Button {
modalType = .donate
} label: {
Label(L10n.Organizer.Items.Donate.caption, systemImage: themeDonateImage)
}.disabled(!productManager.canMakePayments())
Button {
URL.openURL(redditURL)
} label: {
Label(L10n.Organizer.Items.JoinCommunity.caption, systemImage: themeRedditImage)
}
Button(action: submitReview) {
Label(L10n.Organizer.Items.WriteReview.caption, systemImage: themeWriteReviewImage)
}
}
}
private var shareMenu: some View {
Group {
Button(L10n.About.Items.ShareTwitter.caption, action: shareOnTwitter)
Button(L10n.About.Items.ShareGeneric.caption, action: shareWithFriend)
Button(Unlocalized.About.alternativeTo, action: shareAlternativeTo)
}
}
private var aboutButton: some View {
Button(L10n.Organizer.Items.About.caption(appName)) {
presentAbout()
}
}
private func presentShortcutsOrPaywall() {
// eligibility: enter Siri shortcuts or present paywall
if isEligibleForSiri {
modalType = .shortcuts
} else {
modalType = .presentPaywallShortcuts
}
}
private func shareOnTwitter() {
let url = Unlocalized.Social.twitterIntent(withMessage: shareMessage)
URL.openURL(url)
}
private func shareWithFriend() {
let shareMessage = "\(shareMessage) \(Constants.URLs.website)"
modalType = .share([shareMessage])
}
private func shareAlternativeTo() {
URL.openURL(alternativeToURL)
}
private func submitReview() {
let reviewURL = Reviewer.urlForReview(withAppId: Constants.App.appStoreId)
URL.openURL(reviewURL)
}
private func presentAbout() {
modalType = .about
}
}
}

View File

@ -1,70 +0,0 @@
//
// OrganizerView+Scene.swift
// Passepartout
//
// Created by Davide De Rosa on 4/2/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
extension OrganizerView {
struct ShortcutsSection: View {
@ObservedObject private var productManager: ProductManager
private var isEligibleForSiri: Bool {
productManager.isEligible(forFeature: .siriShortcuts)
}
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
productManager = .shared
_modalType = modalType
}
var body: some View {
Section(
header: Text(Unlocalized.Other.siri),
footer: Text(L10n.Organizer.Sections.Siri.footer)
) {
// eligibility: enter Siri shortcuts or present paywall
if isEligibleForSiri {
NavigationLink {
ShortcutsView()
} label: {
shortcutsRow
}
} else {
Button {
modalType = .presentPaywallShortcuts
} label: {
shortcutsRow
}
}
}
}
private var shortcutsRow: some View {
// Text(L10n.Organizer.Items.SiriShortcuts.caption)
Label(L10n.Organizer.Items.SiriShortcuts.caption, systemImage: themeShortcutsImage)
}
}
}

View File

@ -1,63 +0,0 @@
//
// OrganizerView+VPN.swift
// Passepartout
//
// Created by Davide De Rosa on 4/2/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
extension OrganizerView {
struct RemoveVPNSection: View {
@ObservedObject private var vpnManager: VPNManager
@State private var isAskingUninstallVPN = false
init() {
vpnManager = .shared
}
var body: some View {
Section {
Button {
isAskingUninstallVPN = true
} label: {
Label(L10n.Organizer.Items.Uninstall.caption, systemImage: themeDeleteImage)
}.foregroundColor(themeErrorColor)
.actionSheet(isPresented: $isAskingUninstallVPN) {
ActionSheet(
title: Text(L10n.Organizer.Alerts.UninstallVpn.message),
message: nil,
buttons: [
.destructive(Text(L10n.Organizer.Items.Uninstall.caption), action: {
Task {
await vpnManager.uninstall()
}
}),
.cancel(Text(L10n.Global.Strings.cancel))
]
)
}
}
}
}
}

View File

@ -24,7 +24,6 @@
//
import SwiftUI
import StoreKit
import PassepartoutCore
struct OrganizerView: View {
@ -33,6 +32,14 @@ struct OrganizerView: View {
case addHost(URL, Bool)
case shortcuts
case donate
case share([Any])
case about
case presentPaywallShortcuts
// XXX: alert ids
@ -42,7 +49,15 @@ struct OrganizerView: View {
case .addHost: return 2
case .presentPaywallShortcuts: return 3
case .shortcuts: return 3
case .donate: return 4
case .share: return 5
case .about: return 6
case .presentPaywallShortcuts: return 7
}
}
}
@ -72,18 +87,14 @@ struct OrganizerView: View {
@AppStorage(AppManager.DefaultKey.didHandleSubreddit.rawValue) var didHandleSubreddit = false
init() {
appManager = .shared
}
private let hostFileTypes = Constants.URLs.filetypes
private let redditURL = Constants.URLs.subreddit
private let appName = Unlocalized.appName
private let versionString = Constants.Global.appVersionString
init() {
appManager = .shared
}
var body: some View {
debugChanges()
return ZStack {
@ -91,22 +102,7 @@ struct OrganizerView: View {
alertType: $alertType,
didHandleSubreddit: $didHandleSubreddit
)
List {
ProfilesSection(
addProfileMenuBindings: .init(
modalType: $modalType,
alertType: $alertType,
isHostFileImporterPresented: $isHostFileImporterPresented
)
)
ShortcutsSection(
modalType: $modalType
)
supportSection
aboutSection
RemoveVPNSection()
// betaSection
}
ProfilesList(alertType: $alertType)
}.navigationTitle(Unlocalized.appName)
.toolbar(content: toolbar)
.sheet(item: $modalType, content: presentedModal)
@ -119,18 +115,20 @@ struct OrganizerView: View {
).onOpenURL(perform: onOpenURL)
}
private func toolbar() -> some View {
Menu {
AddProfileMenu(
withImportedURLs: true,
bindings: .init(
modalType: $modalType,
alertType: $alertType,
isHostFileImporterPresented: $isHostFileImporterPresented
)
@ToolbarContentBuilder
private func toolbar() -> some ToolbarContent {
ToolbarItem(placement: .primaryAction) {
AddMenu(
modalType: $modalType,
isHostFileImporterPresented: $isHostFileImporterPresented
)
} label: {
themeAddProfileImage.asSystemImage
}
ToolbarItemGroup(placement: .automatic) {
SettingsMenu(
modalType: $modalType,
alertType: $alertType
)
EditButton() // FIXME: toolbars, this is not shown
}
}
}
@ -161,6 +159,24 @@ extension OrganizerView {
)
)
}.themeGlobal()
case .shortcuts:
NavigationView {
ShortcutsView()
}.themeGlobal()
case .donate:
NavigationView {
DonateView()
}.themeGlobal()
case .share(let items):
ActivityView(activityItems: items)
case .about:
NavigationView {
AboutView()
}.themeGlobal()
case .presentPaywallShortcuts:
NavigationView {
@ -228,51 +244,10 @@ extension OrganizerView {
}
}
// MARK: Minor sections
extension OrganizerView {
private var supportSection: some View {
Section(
header: Text(L10n.Organizer.Sections.Support.header)
) {
NavigationLink {
DonateView()
} label: {
Label(L10n.Organizer.Items.Donate.caption, systemImage: themeDonateImage)
}.disabled(!SKPaymentQueue.canMakePayments())
Button {
URL.openURL(redditURL)
} label: {
Label(L10n.Organizer.Items.JoinCommunity.caption, systemImage: themeRedditImage)
}
Button(action: submitReview) {
Label(L10n.Organizer.Items.WriteReview.caption, systemImage: themeWriteReviewImage)
}
}
}
private var aboutSection: some View {
Section {
NavigationLink {
AboutView()
} label: {
Text(L10n.Organizer.Items.About.caption(appName))
// .withTrailingText(versionString)
}
}
}
}
// MARK: Actions
extension OrganizerView {
private func presentSubscribeReddit() {
alertType = .subscribeReddit
}
private func submitReview() {
let reviewURL = Reviewer.urlForReview(withAppId: Constants.App.appStoreId)
URL.openURL(reviewURL)
}
}

View File

@ -124,4 +124,38 @@ extension ProfileView {
}
}
}
struct UninstallVPNSection: View {
@ObservedObject private var vpnManager: VPNManager
@State private var isAskingUninstallVPN = false
init() {
vpnManager = .shared
}
var body: some View {
Section {
Button {
isAskingUninstallVPN = true
} label: {
Label(L10n.Organizer.Items.Uninstall.caption, systemImage: themeDeleteImage)
}.foregroundColor(themeErrorColor)
.actionSheet(isPresented: $isAskingUninstallVPN) {
ActionSheet(
title: Text(L10n.Organizer.Alerts.UninstallVpn.message),
message: nil,
buttons: [
.destructive(Text(L10n.Organizer.Items.Uninstall.caption), action: {
Task {
await vpnManager.uninstall()
}
}),
.cancel(Text(L10n.Global.Strings.cancel))
]
)
}
}
}
}
}

View File

@ -30,6 +30,7 @@ extension ProfileView {
var body: some View {
Text(L10n.Profile.Welcome.message)
.multilineTextAlignment(.center)
.themeInformativeText()
}
}
}

View File

@ -99,6 +99,7 @@ struct ProfileView: View {
ExtraSection(currentProfile: profileManager.currentProfile)
DiagnosticsSection(currentProfile: profileManager.currentProfile)
removeProfileSection
UninstallVPNSection()
}
private var welcomeView: some View {
@ -171,6 +172,27 @@ struct ProfileView: View {
}
}
private func confirmRemoveProfile() {
withAnimation {
removeProfile()
}
}
private func removeProfile() {
guard profileManager.isExistingProfile(withId: header.id) else {
assertionFailure("Deleting non-existent profile \(header.name)")
return
}
IntentDispatcher.forgetProfile(withHeader: header)
profileManager.removeProfiles(withIds: [header.id])
// XXX: iOS 14, NavigationLink removal via header removal in OrganizerView+Profiles doesn't pop
if #available(iOS 15, *) {
} else {
presentationMode.wrappedValue.dismiss()
}
}
private func loadProfileIfNeeded() {
guard !isLoaded else {
return
@ -201,27 +223,6 @@ struct ProfileView: View {
presentationMode.wrappedValue.dismiss()
}
}
private func confirmRemoveProfile() {
withAnimation {
removeProfile()
}
}
private func removeProfile() {
guard profileManager.isExistingProfile(withId: header.id) else {
assertionFailure("Deleting non-existent profile \(header.name)")
return
}
IntentDispatcher.forgetProfile(withHeader: header)
profileManager.removeProfiles(withIds: [header.id])
// XXX: iOS 14, NavigationLink removal via header removal in OrganizerView+Profiles doesn't pop
if #available(iOS 15, *) {
} else {
presentationMode.wrappedValue.dismiss()
}
}
private func presentPaywallTrustedNetworks() {
modalType = .paywallTrustedNetworks

View File

@ -80,7 +80,10 @@ struct ShortcutsView: View {
}
private var addSection: some View {
Section {
Section(
// FIXME: l10n, string id
footer: Text(L10n.Organizer.Sections.Siri.footer)
) {
NavigationLink(isActive: $isNavigationPresented) {
AddView(
pendingShortcut: delegatingPendingShortcut