Group profile actions into menus

- Organizer
    - Duplicate

- Profile
    - Rename
    - Siri
    - Uninstall (+ confirmation)
    - Delete (+ confirmation)
This commit is contained in:
Davide De Rosa 2022-04-26 23:27:03 +02:00
parent 1c047b9ce2
commit cd854f8ebf
11 changed files with 228 additions and 223 deletions

View File

@ -34,7 +34,7 @@
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 */; };
0E3CD483280DAE92007075C0 /* ProfileView+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3CD482280DAE92007075C0 /* ProfileView+Toolbar.swift */; };
0E3CD483280DAE92007075C0 /* ProfileView+MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3CD482280DAE92007075C0 /* ProfileView+MainMenu.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 */; };
@ -64,7 +64,6 @@
0E71ACFB27C12E5300F85C4B /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFA27C12E5300F85C4B /* VersionView.swift */; };
0E71ACFD27C1321A00F85C4B /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFC27C1321A00F85C4B /* ActivityView.swift */; };
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577D62816A3B200081CBE /* DestructiveButton.swift */; };
0E7577DD2816C3AD00081CBE /* ProfileView+Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577DC2816C3AD00081CBE /* ProfileView+Buttons.swift */; };
0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577DE2817E22C00081CBE /* VPNToggle.swift */; };
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */; };
0E92D7C627F103300033CB7B /* ProfileView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */; };
@ -220,7 +219,7 @@
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>"; };
0E3CD482280DAE92007075C0 /* ProfileView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Toolbar.swift"; sourceTree = "<group>"; };
0E3CD482280DAE92007075C0 /* ProfileView+MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+MainMenu.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>"; };
@ -251,7 +250,6 @@
0E71ACFA27C12E5300F85C4B /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; };
0E71ACFC27C1321A00F85C4B /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
0E7577D62816A3B200081CBE /* DestructiveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveButton.swift; sourceTree = "<group>"; };
0E7577DC2816C3AD00081CBE /* ProfileView+Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Buttons.swift"; sourceTree = "<group>"; };
0E7577DE2817E22C00081CBE /* VPNToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggle.swift; sourceTree = "<group>"; };
0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostViewModel.swift; sourceTree = "<group>"; };
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Configuration.swift"; sourceTree = "<group>"; };
@ -454,13 +452,12 @@
0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */,
0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */,
0E44689527B051C300A14CE4 /* ProfileView.swift */,
0E7577DC2816C3AD00081CBE /* ProfileView+Buttons.swift */,
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
0E92D7F327F104B80033CB7B /* ProfileView+Diagnostics.swift */,
0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */,
0E3CD482280DAE92007075C0 /* ProfileView+MainMenu.swift */,
0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */,
0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */,
0E3CD482280DAE92007075C0 /* ProfileView+Toolbar.swift */,
0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */,
0E71ACF027C1073800F85C4B /* ProviderLocationView.swift */,
0E71ACEE27C106B400F85C4B /* ProviderPresetView.swift */,
@ -902,7 +899,6 @@
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */,
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
0EF708322811CC8400A3A308 /* VPNStatusText.swift in Sources */,
0E7577DD2816C3AD00081CBE /* ProfileView+Buttons.swift in Sources */,
0E5324A627D297BB002565C3 /* InApp.swift in Sources */,
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
@ -967,7 +963,7 @@
0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */,
0ED89C1527DE0A0C008B36D6 /* Shortcut.swift in Sources */,
0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */,
0E3CD483280DAE92007075C0 /* ProfileView+Toolbar.swift in Sources */,
0E3CD483280DAE92007075C0 /* ProfileView+MainMenu.swift in Sources */,
0EB17EAE27D226CF00D473B5 /* LocalProduct.swift in Sources */,
0E71ACEB27C1060D00F85C4B /* EndpointView.swift in Sources */,
0E53249927D26B51002565C3 /* ProductManager.swift in Sources */,

View File

@ -126,7 +126,7 @@
<EnvironmentVariables>
<EnvironmentVariable
key = "APP_TYPE"
value = "0"
value = "2"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable

View File

@ -437,6 +437,8 @@ internal enum L10n {
internal static let configuration = L10n.tr("Localizable", "global.strings.configuration")
/// Default
internal static let `default` = L10n.tr("Localizable", "global.strings.default")
/// Delete
internal static let delete = L10n.tr("Localizable", "global.strings.delete")
/// Disabled
internal static let disabled = L10n.tr("Localizable", "global.strings.disabled")
/// Domain
@ -493,6 +495,8 @@ internal enum L10n {
internal static let servers = L10n.tr("Localizable", "global.strings.servers")
/// Translations
internal static let translations = L10n.tr("Localizable", "global.strings.translations")
/// Uninstall
internal static let uninstall = L10n.tr("Localizable", "global.strings.uninstall")
}
}
@ -694,8 +698,6 @@ internal enum L10n {
internal static func message(_ p1: Any) -> String {
return L10n.tr("Localizable", "organizer.alerts.remove_profile.message", String(describing: p1))
}
/// Remove profile
internal static let title = L10n.tr("Localizable", "organizer.alerts.remove_profile.title")
}
}
internal enum Empty {
@ -815,10 +817,6 @@ internal enum L10n {
/// Reconnect
internal static let caption = L10n.tr("Localizable", "profile.items.reconnect.caption")
}
internal enum Uninstall {
/// Remove VPN configuration
internal static let caption = L10n.tr("Localizable", "profile.items.uninstall.caption")
}
internal enum UseProfile {
/// Use this profile
internal static let caption = L10n.tr("Localizable", "profile.items.use_profile.caption")

View File

@ -172,6 +172,10 @@ extension View {
"xmark"
}
var themeUninstallImage: String {
"arrow.uturn.down"
}
var themeDeleteImage: String {
"trash.fill"
}
@ -281,10 +285,6 @@ extension View {
foregroundColor(themeLightTextColor)
}
func themeDestructiveButtonStyle() -> some View {
foregroundColor(themeErrorColor)
}
@available(iOS 15, *)
func themePrimaryTintStyle() -> some View {
tint(themePrimaryBackgroundColor)

View File

@ -109,11 +109,7 @@ extension OrganizerView {
isActive: profileManager.isActiveProfile(header.id)
)
}.contextMenu {
Button {
duplicateProfile(withId: header.id)
} label: {
Label(L10n.Global.Strings.duplicate, systemImage: themeDuplicateImage)
}
ProfileView.DuplicateButton(header: header)
}.themeTextButtonStyle()
}
@ -194,10 +190,6 @@ extension OrganizerView.ProfilesList {
profileManager.removeProfiles(withIds: toDelete)
}
private func duplicateProfile(withId id: UUID) {
profileManager.duplicateProfile(withId: id)
}
private func performMigrationsIfNeeded() {
Task {
await appManager.doMigrations(profileManager)

View File

@ -1,98 +0,0 @@
//
// ProfileView+Buttons.swift
// Passepartout
//
// Created by Davide De Rosa on 4/25/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 ProfileView {
struct RemoveProfileButton: View {
@ObservedObject private var profileManager: ProfileManager
private let header: Profile.Header
@State private var isConfirming = false
private let title = L10n.Organizer.Alerts.RemoveProfile.title
init(header: Profile.Header) {
profileManager = .shared
self.header = header
}
var body: some View {
DestructiveButton {
isConfirming = true
} label: {
Label(title, systemImage: themeDeleteImage)
}.actionSheet(isPresented: $isConfirming) {
ActionSheet(
title: Text(L10n.Organizer.Alerts.RemoveProfile.message(header.name)),
message: nil,
buttons: [
.destructive(Text(title), action: removeProfile),
.cancel(Text(L10n.Global.Strings.cancel))
]
)
}.themeDestructiveButtonStyle()
}
private func removeProfile() {
profileManager.removeProfiles(withIds: [header.id])
}
}
struct UninstallVPNButton: View {
@ObservedObject private var vpnManager: VPNManager
@State private var isConfirming = false
init() {
vpnManager = .shared
}
var body: some View {
DestructiveButton {
isConfirming = true
} label: {
Label(L10n.Profile.Items.Uninstall.caption, systemImage: themeDeleteImage)
}.actionSheet(isPresented: $isConfirming) {
ActionSheet(
title: Text(L10n.Profile.Alerts.UninstallVpn.message),
message: nil,
buttons: [
.destructive(Text(L10n.Profile.Items.Uninstall.caption), action: uninstallVPN),
.cancel(Text(L10n.Global.Strings.cancel))
]
)
}.themeDestructiveButtonStyle()
}
private func uninstallVPN() {
Task {
await vpnManager.uninstall()
}
}
}
}

View File

@ -0,0 +1,208 @@
//
// ProfileView+MainMenu.swift
// Passepartout
//
// Created by Davide De Rosa on 2/6/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 ProfileView {
struct MainMenu: View {
enum ActionSheetType: Int, Identifiable {
case uninstallVPN
case deleteProfile
var id: Int {
rawValue
}
}
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var vpnManager: VPNManager
@ObservedObject private var currentProfile: ObservableProfile
private var header: Profile.Header {
currentProfile.value.header
}
@Binding private var modalType: ModalType?
@State private var actionSheetType: ActionSheetType?
private let uninstallVPNTitle = L10n.Global.Strings.uninstall
private let deleteProfileTitle = L10n.Global.Strings.delete
private let cancelTitle = L10n.Global.Strings.cancel
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
profileManager = .shared
vpnManager = .shared
self.currentProfile = currentProfile
_modalType = modalType
}
var body: some View {
Menu {
RenameButton(
modalType: $modalType
)
// should contextually set duplicate as current profile
// DuplicateButton(
// header: currentProfile.value.header
// )
ShortcutsButton(
modalType: $modalType
)
uninstallVPNButton
Divider()
deleteProfileButton
} label: {
themeSettingsMenuImage.asSystemImage
}.actionSheet(item: $actionSheetType) {
switch $0 {
case .uninstallVPN:
return ActionSheet(
title: Text(L10n.Profile.Alerts.UninstallVpn.message),
message: nil,
buttons: [
.destructive(Text(uninstallVPNTitle), action: uninstallVPN),
.cancel(Text(cancelTitle))
]
)
case .deleteProfile:
return ActionSheet(
title: Text(L10n.Organizer.Alerts.RemoveProfile.message(header.name)),
message: nil,
buttons: [
.destructive(Text(deleteProfileTitle), action: removeProfile),
.cancel(Text(cancelTitle))
]
)
}
}
}
private var uninstallVPNButton: some View {
Button {
actionSheetType = .uninstallVPN
} label: {
Label(uninstallVPNTitle, systemImage: themeUninstallImage)
}
}
private var deleteProfileButton: some View {
DestructiveButton {
actionSheetType = .deleteProfile
} label: {
Label(deleteProfileTitle, systemImage: themeDeleteImage)
}
}
private func uninstallVPN() {
Task {
await vpnManager.uninstall()
}
}
private func removeProfile() {
profileManager.removeProfiles(withIds: [header.id])
}
}
struct RenameButton: View {
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
_modalType = modalType
}
var body: some View {
Button {
modalType = .rename
} label: {
Label(L10n.Global.Strings.rename, systemImage: themeRenameProfileImage)
}
}
}
struct ShortcutsButton: View {
@ObservedObject private var productManager: ProductManager
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
productManager = .shared
_modalType = modalType
}
private var isEligibleForSiri: Bool {
productManager.isEligible(forFeature: .siriShortcuts)
}
var body: some View {
Button {
presentShortcutsOrPaywall()
} label: {
Label(Unlocalized.Other.siri, systemImage: themeShortcutsImage)
}
}
private func presentShortcutsOrPaywall() {
// eligibility: enter Siri shortcuts or present paywall
if isEligibleForSiri {
modalType = .shortcuts
} else {
modalType = .paywallShortcuts
}
}
}
struct DuplicateButton: View {
@ObservedObject private var profileManager: ProfileManager
let header: Profile.Header
init(header: Profile.Header) {
profileManager = .shared
self.header = header
}
var body: some View {
Button {
duplicateProfile(withId: header.id)
} label: {
Label(L10n.Global.Strings.duplicate, systemImage: themeDuplicateImage)
}
}
private func duplicateProfile(withId id: UUID) {
profileManager.duplicateProfile(withId: id)
}
}
}

View File

@ -1,81 +0,0 @@
//
// ProfileView+Toolbar.swift
// Passepartout
//
// Created by Davide De Rosa on 2/6/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 ProfileView {
struct ShortcutsItem: View {
@ObservedObject private var productManager: ProductManager
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
productManager = .shared
_modalType = modalType
}
private var isEligibleForSiri: Bool {
productManager.isEligible(forFeature: .siriShortcuts)
}
var body: some View {
Button {
presentShortcutsOrPaywall()
} label: {
themeShortcutsImage.asSystemImage
}
}
private func presentShortcutsOrPaywall() {
// eligibility: enter Siri shortcuts or present paywall
if isEligibleForSiri {
modalType = .shortcuts
} else {
modalType = .paywallShortcuts
}
}
}
struct RenameItem: View {
@ObservedObject private var currentProfile: ObservableProfile
@Binding private var modalType: ModalType?
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
self.currentProfile = currentProfile
_modalType = modalType
}
var body: some View {
Button {
modalType = .rename
} label: {
themeRenameProfileImage.asSystemImage
}
}
}
}

View File

@ -70,16 +70,10 @@ struct ProfileView: View {
WelcomeView()
}
}.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ShortcutsItem(
modalType: $modalType
).disabled(!isExisting)
RenameItem(
currentProfile: profileManager.currentProfile,
modalType: $modalType
).disabled(!isExisting)
}
MainMenu(
currentProfile: profileManager.currentProfile,
modalType: $modalType
).disabled(!isExisting)
}.sheet(item: $modalType, content: presentedModal)
.navigationTitle(title)
.themeSecondaryView()
@ -103,10 +97,6 @@ struct ProfileView: View {
)
ExtraSection(currentProfile: profileManager.currentProfile)
DiagnosticsSection(currentProfile: profileManager.currentProfile)
Section {
UninstallVPNButton()
RemoveProfileButton(header: profileManager.currentProfile.value.header)
}
}
}
}

View File

@ -14,6 +14,8 @@
"global.strings.rename" = "Rename";
"global.strings.duplicate" = "Duplicate";
"global.strings.add" = "Add";
"global.strings.delete" = "Delete";
"global.strings.uninstall" = "Uninstall";
"global.strings.default" = "Default";
"global.strings.name" = "Name";
"global.strings.profiles" = "Profiles";
@ -123,7 +125,6 @@
"organizer.alerts.reddit.buttons.remind" = "Remind me later";
"organizer.alerts.reddit.buttons.never" = "Don't ask again";
"organizer.alerts.remove_profile.title" = "Remove profile";
"organizer.alerts.remove_profile.message" = "Are you sure you want to delete profile %@?";
/* MARK: AddProfileView */
@ -164,7 +165,6 @@
"profile.items.vpn_survives_sleep.caption" = "Keep alive on sleep";
"profile.items.vpn_resolves_hostname.caption" = "Resolve provider hostname";
"profile.items.reconnect.caption" = "Reconnect";
"profile.items.uninstall.caption" = "Remove VPN configuration";
"profile.alerts.rename.title" = "Rename profile";
"profile.alerts.reconnect_vpn.message" = "Do you want to reconnect to the VPN?";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 222 KiB