Decouple menus from OrganizerView
Could move .sheet() from parent View to Menu, but no luck with .fileImporter()
This commit is contained in:
parent
d89130bc3a
commit
6fddbb8bfc
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OrganizerView+AddMenu.swift
|
||||
// AddProfileMenu.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 4/18/22.
|
||||
|
@ -26,85 +26,135 @@
|
|||
import SwiftUI
|
||||
import PassepartoutCore
|
||||
|
||||
extension OrganizerView {
|
||||
struct AddMenu: View {
|
||||
@Binding private var modalType: ModalType?
|
||||
struct AddProfileMenu: View {
|
||||
enum ModalType: Identifiable {
|
||||
case addProvider
|
||||
|
||||
case addHost(URL, Bool)
|
||||
|
||||
@Binding private var isHostFileImporterPresented: Bool
|
||||
|
||||
init(modalType: Binding<ModalType?>, isHostFileImporterPresented: Binding<Bool>) {
|
||||
_modalType = modalType
|
||||
_isHostFileImporterPresented = isHostFileImporterPresented
|
||||
// XXX: alert ids
|
||||
var id: Int {
|
||||
switch self {
|
||||
case .addProvider: return 1
|
||||
|
||||
case .addHost: return 2
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button {
|
||||
modalType = .addProvider
|
||||
} label: {
|
||||
Label(L10n.Global.Strings.provider, systemImage: themeProviderImage)
|
||||
}
|
||||
Button {
|
||||
presentHostFileImporter()
|
||||
} label: {
|
||||
Label(L10n.Menu.Contextual.AddProfile.fromFiles, systemImage: themeHostFilesImage)
|
||||
}
|
||||
}
|
||||
|
||||
@Binding private var modalType: ModalType?
|
||||
|
||||
@Binding private var isHostFileImporterPresented: Bool
|
||||
|
||||
init(modalType: Binding<ModalType?>, isHostFileImporterPresented: Binding<Bool>) {
|
||||
_modalType = modalType
|
||||
_isHostFileImporterPresented = isHostFileImporterPresented
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button {
|
||||
modalType = .addProvider
|
||||
} label: {
|
||||
Label(L10n.Global.Strings.provider, systemImage: themeProviderImage)
|
||||
}
|
||||
Button {
|
||||
presentHostFileImporter()
|
||||
} label: {
|
||||
Label(L10n.Menu.Contextual.AddProfile.fromFiles, systemImage: themeHostFilesImage)
|
||||
}
|
||||
// Button {
|
||||
// // TODO: add profile from text
|
||||
// } label: {
|
||||
// Label(L10n.Organizer.Menus.AddProfile.fromText, systemImage: themeHostTextImage)
|
||||
// }
|
||||
if let urls = importedURLs, !urls.isEmpty {
|
||||
Divider()
|
||||
ForEach(urls, id: \.absoluteString, content: importedURLRow)
|
||||
}
|
||||
} label: {
|
||||
themeAddMenuImage.asSystemImage
|
||||
if let urls = importedURLs, !urls.isEmpty {
|
||||
Divider()
|
||||
ForEach(urls, id: \.absoluteString, content: importedURLRow)
|
||||
}
|
||||
} label: {
|
||||
themeAddMenuImage.asSystemImage
|
||||
}.sheet(item: $modalType, content: presentedModal)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||
switch modalType {
|
||||
case .addProvider:
|
||||
NavigationView {
|
||||
AddProviderView(
|
||||
bindings: .init(
|
||||
isPresented: isModalPresented
|
||||
)
|
||||
)
|
||||
}.themeGlobal()
|
||||
|
||||
case .addHost(let url, let deletingURLOnSuccess):
|
||||
NavigationView {
|
||||
AddHostView.NameView(
|
||||
url: url,
|
||||
deletingURLOnSuccess: deletingURLOnSuccess,
|
||||
bindings: .init(
|
||||
isPresented: isModalPresented
|
||||
)
|
||||
)
|
||||
}.themeGlobal()
|
||||
}
|
||||
}
|
||||
|
||||
private var isModalPresented: Binding<Bool> {
|
||||
.init {
|
||||
modalType != nil
|
||||
} set: {
|
||||
if !$0 {
|
||||
modalType = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func importedURLRow(_ url: URL) -> some View {
|
||||
Button(L10n.Menu.Contextual.AddProfile.imported(url.lastPathComponent)) {
|
||||
presentAddHost(withURL: url, deletingURLOnSuccess: true)
|
||||
private func importedURLRow(_ url: URL) -> some View {
|
||||
Button(L10n.Menu.Contextual.AddProfile.imported(url.lastPathComponent)) {
|
||||
presentAddHost(withURL: url, deletingURLOnSuccess: true)
|
||||
}
|
||||
}
|
||||
|
||||
private var importedURLs: [URL]? {
|
||||
do {
|
||||
let url = FileManager.default.userURL(for: .documentDirectory, appending: nil)
|
||||
let list = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
||||
return list.filter {
|
||||
VPNProtocolType.knownFileExtensions.contains($0.pathExtension)
|
||||
}
|
||||
}
|
||||
|
||||
private var importedURLs: [URL]? {
|
||||
do {
|
||||
let url = FileManager.default.userURL(for: .documentDirectory, appending: nil)
|
||||
let list = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
||||
return list.filter {
|
||||
VPNProtocolType.knownFileExtensions.contains($0.pathExtension)
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func presentAddProvider() {
|
||||
modalType = .addProvider
|
||||
}
|
||||
|
||||
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)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AddProfileMenu {
|
||||
private func presentAddProvider() {
|
||||
modalType = .addProvider
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OrganizerView+SettingsMenu.swift
|
||||
// InfoMenu.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 4/18/22.
|
||||
|
@ -26,103 +26,152 @@
|
|||
import SwiftUI
|
||||
import PassepartoutCore
|
||||
|
||||
extension OrganizerView {
|
||||
struct SettingsMenu: View {
|
||||
@ObservedObject private var productManager: ProductManager
|
||||
struct InfoMenu: View {
|
||||
enum ModalType: Identifiable {
|
||||
case donate
|
||||
|
||||
@Binding var modalType: ModalType?
|
||||
case share([Any])
|
||||
|
||||
@Binding var alertType: AlertType?
|
||||
case about
|
||||
|
||||
private var isTestBuild: Bool {
|
||||
Constants.App.isBeta || Constants.InApp.appType == .beta
|
||||
case exportProviders([URL])
|
||||
|
||||
// XXX: alert ids
|
||||
var id: Int {
|
||||
switch self {
|
||||
case .donate: return 4
|
||||
|
||||
case .share: return 5
|
||||
|
||||
case .about: return 6
|
||||
|
||||
case .exportProviders: return 7
|
||||
}
|
||||
}
|
||||
|
||||
private let redditURL = Constants.URLs.subreddit
|
||||
|
||||
private let shareMessage = L10n.Global.Messages.share
|
||||
}
|
||||
|
||||
@ObservedObject private var productManager: ProductManager
|
||||
|
||||
@State private var modalType: ModalType?
|
||||
|
||||
private var isTestBuild: Bool {
|
||||
Constants.App.isBeta || Constants.InApp.appType == .beta
|
||||
}
|
||||
|
||||
private let redditURL = Constants.URLs.subreddit
|
||||
|
||||
private let shareMessage = L10n.Global.Messages.share
|
||||
|
||||
private let appName = Unlocalized.appName
|
||||
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.All.Support.title) {
|
||||
supportMenu
|
||||
}
|
||||
Menu(L10n.Menu.All.Share.title) {
|
||||
shareMenu
|
||||
}
|
||||
if isTestBuild {
|
||||
Divider()
|
||||
testSection
|
||||
}
|
||||
init() {
|
||||
productManager = .shared
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Menu(L10n.Menu.All.Support.title) {
|
||||
supportMenu
|
||||
}
|
||||
Menu(L10n.Menu.All.Share.title) {
|
||||
shareMenu
|
||||
}
|
||||
if isTestBuild {
|
||||
Divider()
|
||||
aboutButton
|
||||
testSection
|
||||
}
|
||||
Divider()
|
||||
aboutButton
|
||||
} label: {
|
||||
themeInfoMenuImage.asSystemImage
|
||||
}.sheet(item: $modalType, content: presentedModal)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||
switch modalType {
|
||||
case .donate:
|
||||
NavigationView {
|
||||
DonateView()
|
||||
}.themeGlobal()
|
||||
|
||||
case .share(let items):
|
||||
ActivityView(activityItems: items)
|
||||
|
||||
case .about:
|
||||
NavigationView {
|
||||
AboutView()
|
||||
}.themeGlobal()
|
||||
|
||||
case .exportProviders(let urls):
|
||||
ActivityView(activityItems: urls)
|
||||
}
|
||||
}
|
||||
|
||||
private var isModalPresented: Binding<Bool> {
|
||||
.init {
|
||||
modalType != nil
|
||||
} set: {
|
||||
if !$0 {
|
||||
modalType = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var supportMenu: some View {
|
||||
Group {
|
||||
Button {
|
||||
modalType = .donate
|
||||
} label: {
|
||||
themeInfoMenuImage.asSystemImage
|
||||
Label(L10n.Donate.title, systemImage: themeDonateImage)
|
||||
}.disabled(!productManager.canMakePayments())
|
||||
|
||||
Button {
|
||||
URL.openURL(redditURL)
|
||||
} label: {
|
||||
Label(L10n.Menu.Contextual.Support.joinCommunity, systemImage: themeRedditImage)
|
||||
}
|
||||
Button(action: submitReview) {
|
||||
Label(L10n.Menu.Contextual.Support.writeReview, systemImage: themeWriteReviewImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var supportMenu: some View {
|
||||
Group {
|
||||
Button {
|
||||
modalType = .donate
|
||||
} label: {
|
||||
Label(L10n.Donate.title, systemImage: themeDonateImage)
|
||||
}.disabled(!productManager.canMakePayments())
|
||||
private var shareMenu: some View {
|
||||
Group {
|
||||
Button(L10n.Menu.Contextual.shareTwitter, action: shareOnTwitter)
|
||||
Button(L10n.Menu.Contextual.shareGeneric, action: shareWithFriend)
|
||||
}
|
||||
}
|
||||
|
||||
private var aboutButton: some View {
|
||||
Button(L10n.Menu.All.About.title("")) {
|
||||
presentAbout()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
URL.openURL(redditURL)
|
||||
} label: {
|
||||
Label(L10n.Menu.Contextual.Support.joinCommunity, systemImage: themeRedditImage)
|
||||
}
|
||||
Button(action: submitReview) {
|
||||
Label(L10n.Menu.Contextual.Support.writeReview, systemImage: themeWriteReviewImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
private func shareOnTwitter() {
|
||||
let url = Unlocalized.Social.twitterIntent(withMessage: shareMessage)
|
||||
URL.openURL(url)
|
||||
}
|
||||
|
||||
private var shareMenu: some View {
|
||||
Group {
|
||||
Button(L10n.Menu.Contextual.shareTwitter, action: shareOnTwitter)
|
||||
Button(L10n.Menu.Contextual.shareGeneric, action: shareWithFriend)
|
||||
}
|
||||
}
|
||||
|
||||
private var aboutButton: some View {
|
||||
Button(L10n.Menu.All.About.title("")) {
|
||||
presentAbout()
|
||||
}
|
||||
}
|
||||
private func shareWithFriend() {
|
||||
let shareMessage = "\(shareMessage) \(Constants.URLs.website)"
|
||||
modalType = .share([shareMessage])
|
||||
}
|
||||
|
||||
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 submitReview() {
|
||||
let reviewURL = Reviewer.urlForReview(withAppId: Constants.App.appStoreId)
|
||||
URL.openURL(reviewURL)
|
||||
}
|
||||
|
||||
private func presentAbout() {
|
||||
modalType = .about
|
||||
}
|
||||
private func submitReview() {
|
||||
let reviewURL = Reviewer.urlForReview(withAppId: Constants.App.appStoreId)
|
||||
URL.openURL(reviewURL)
|
||||
}
|
||||
|
||||
private func presentAbout() {
|
||||
modalType = .about
|
||||
}
|
||||
}
|
||||
|
||||
extension OrganizerView.SettingsMenu {
|
||||
extension InfoMenu {
|
||||
private var testSection: some View {
|
||||
Button("Export providers") {
|
||||
guard let urls = AppContext.shared.urlsForProviders else {
|
||||
|
|
|
@ -27,37 +27,6 @@ import SwiftUI
|
|||
import PassepartoutCore
|
||||
|
||||
struct OrganizerView: View {
|
||||
enum ModalType: Identifiable {
|
||||
case addProvider
|
||||
|
||||
case addHost(URL, Bool)
|
||||
|
||||
case donate
|
||||
|
||||
case share([Any])
|
||||
|
||||
case about
|
||||
|
||||
case exportProviders([URL])
|
||||
|
||||
// XXX: alert ids
|
||||
var id: Int {
|
||||
switch self {
|
||||
case .addProvider: return 1
|
||||
|
||||
case .addHost: return 2
|
||||
|
||||
case .donate: return 4
|
||||
|
||||
case .share: return 5
|
||||
|
||||
case .about: return 6
|
||||
|
||||
case .exportProviders: return 7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AlertType: Identifiable {
|
||||
case subscribeReddit
|
||||
|
||||
|
@ -73,7 +42,7 @@ struct OrganizerView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@State private var modalType: ModalType?
|
||||
@State private var addProfileModalType: AddProfileMenu.ModalType?
|
||||
|
||||
@State private var alertType: AlertType?
|
||||
|
||||
|
@ -92,20 +61,15 @@ struct OrganizerView: View {
|
|||
ProfilesList()
|
||||
}.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
AddMenu(
|
||||
modalType: $modalType,
|
||||
AddProfileMenu(
|
||||
modalType: $addProfileModalType,
|
||||
isHostFileImporterPresented: $isHostFileImporterPresented
|
||||
)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
SettingsMenu(
|
||||
modalType: $modalType,
|
||||
alertType: $alertType
|
||||
)
|
||||
// EditButton()
|
||||
ToolbarItem(placement: .navigation) {
|
||||
InfoMenu()
|
||||
}
|
||||
}.sheet(item: $modalType, content: presentedModal)
|
||||
.alert(item: $alertType, content: presentedAlert)
|
||||
}.alert(item: $alertType, content: presentedAlert)
|
||||
.fileImporter(
|
||||
isPresented: $isHostFileImporterPresented,
|
||||
allowedContentTypes: hostFileTypes,
|
||||
|
@ -124,56 +88,25 @@ struct OrganizerView: View {
|
|||
}
|
||||
|
||||
extension OrganizerView {
|
||||
|
||||
@ViewBuilder
|
||||
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||
switch modalType {
|
||||
case .addProvider:
|
||||
NavigationView {
|
||||
AddProviderView(
|
||||
bindings: .init(
|
||||
isPresented: isModalPresented
|
||||
)
|
||||
)
|
||||
}.themeGlobal()
|
||||
private func onHostFileImporterResult(_ result: Result<[URL], Error>) {
|
||||
switch result {
|
||||
case .success(let urls):
|
||||
guard let url = urls.first else {
|
||||
assertionFailure("Empty URLs from file importer?")
|
||||
return
|
||||
}
|
||||
addProfileModalType = .addHost(url, false)
|
||||
|
||||
case .addHost(let url, let deletingURLOnSuccess):
|
||||
NavigationView {
|
||||
AddHostView.NameView(
|
||||
url: url,
|
||||
deletingURLOnSuccess: deletingURLOnSuccess,
|
||||
bindings: .init(
|
||||
isPresented: isModalPresented
|
||||
)
|
||||
)
|
||||
}.themeGlobal()
|
||||
|
||||
case .donate:
|
||||
NavigationView {
|
||||
DonateView()
|
||||
}.themeGlobal()
|
||||
|
||||
case .share(let items):
|
||||
ActivityView(activityItems: items)
|
||||
|
||||
case .about:
|
||||
NavigationView {
|
||||
AboutView()
|
||||
}.themeGlobal()
|
||||
|
||||
case .exportProviders(let urls):
|
||||
ActivityView(activityItems: urls)
|
||||
case .failure(let error):
|
||||
alertType = .error(
|
||||
L10n.Menu.Contextual.AddProfile.fromFiles,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var isModalPresented: Binding<Bool> {
|
||||
.init {
|
||||
modalType != nil
|
||||
} set: {
|
||||
if !$0 {
|
||||
modalType = nil
|
||||
}
|
||||
}
|
||||
private func onOpenURL(_ url: URL) {
|
||||
addProfileModalType = .addHost(url, false)
|
||||
}
|
||||
|
||||
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
||||
|
@ -199,27 +132,6 @@ extension OrganizerView {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func onHostFileImporterResult(_ result: Result<[URL], Error>) {
|
||||
switch result {
|
||||
case .success(let urls):
|
||||
guard let url = urls.first else {
|
||||
assertionFailure("Empty URLs from file importer?")
|
||||
return
|
||||
}
|
||||
modalType = .addHost(url, false)
|
||||
|
||||
case .failure(let error):
|
||||
alertType = .error(
|
||||
L10n.Menu.Contextual.AddProfile.fromFiles,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func onOpenURL(_ url: URL) {
|
||||
modalType = .addHost(url, false)
|
||||
}
|
||||
}
|
||||
|
||||
extension OrganizerView {
|
||||
|
|
Loading…
Reference in New Issue