Add SwiftLint phase (#262)
This commit is contained in:
parent
cecf64d871
commit
f06f097f27
|
@ -1047,6 +1047,7 @@
|
||||||
0E41BDA828671339006346B4 /* Embed Launcher */,
|
0E41BDA828671339006346B4 /* Embed Launcher */,
|
||||||
0E3152B7223F9EF500F61841 /* Embed Plugins */,
|
0E3152B7223F9EF500F61841 /* Embed Plugins */,
|
||||||
0EB2B14B2733FB6F007705AB /* Embed Foundation Extensions */,
|
0EB2B14B2733FB6F007705AB /* Embed Foundation Extensions */,
|
||||||
|
0E1B5F5D29C5082700FE7D18 /* SwiftLint */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -1297,6 +1298,24 @@
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
0E1B5F5D29C5082700FE7D18 /* SwiftLint */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = SwiftLint;
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
|
};
|
||||||
0EADDC7227F0677F0093E303 /* Copy Core Data codegen */ = {
|
0EADDC7227F0677F0093E303 /* Copy Core Data codegen */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
|
|
|
@ -28,8 +28,8 @@ import UIKit
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
|
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
|
||||||
private let mac = MacBundle.shared
|
private let mac = MacBundle.shared
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
mac.configure()
|
mac.configure()
|
||||||
mac.menu.install()
|
mac.menu.install()
|
||||||
|
|
|
@ -44,7 +44,7 @@ extension Constants {
|
||||||
Bundle.main.isTestFlight
|
Bundle.main.isTestFlight
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Plugins {
|
enum Plugins {
|
||||||
static let macBridgeName = "PassepartoutMac.bundle"
|
static let macBridgeName = "PassepartoutMac.bundle"
|
||||||
}
|
}
|
||||||
|
@ -92,9 +92,9 @@ extension Constants {
|
||||||
static let disableVPN = "DisableVPNIntent"
|
static let disableVPN = "DisableVPNIntent"
|
||||||
|
|
||||||
static let connectVPN = "ConnectVPNIntent"
|
static let connectVPN = "ConnectVPNIntent"
|
||||||
|
|
||||||
static let moveToLocation = "MoveToLocationIntent"
|
static let moveToLocation = "MoveToLocationIntent"
|
||||||
|
|
||||||
static let trustCellularNetwork = "TrustCellularNetworkIntent"
|
static let trustCellularNetwork = "TrustCellularNetworkIntent"
|
||||||
|
|
||||||
static let trustCurrentNetwork = "TrustCurrentNetworkIntent"
|
static let trustCurrentNetwork = "TrustCurrentNetworkIntent"
|
||||||
|
@ -109,7 +109,7 @@ extension Constants {
|
||||||
enum Domain {
|
enum Domain {
|
||||||
static let name = "passepartoutvpn.app"
|
static let name = "passepartoutvpn.app"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Services {
|
enum Services {
|
||||||
static let version = "v5"
|
static let version = "v5"
|
||||||
|
|
||||||
|
@ -120,38 +120,38 @@ extension Constants {
|
||||||
"https://www.facebook.com",
|
"https://www.facebook.com",
|
||||||
"https://www.instagram.com"
|
"https://www.instagram.com"
|
||||||
]
|
]
|
||||||
|
|
||||||
static let connectivityURL = URL(string: connectivityStrings.randomElement()!)!
|
static let connectivityURL = URL(string: connectivityStrings.randomElement()!)!
|
||||||
|
|
||||||
static let connectivityTimeout: TimeInterval = 10.0
|
static let connectivityTimeout: TimeInterval = 10.0
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Persistence {
|
enum Persistence {
|
||||||
static let profilesContainerName = "Profiles"
|
static let profilesContainerName = "Profiles"
|
||||||
|
|
||||||
static let providersContainerName = "Providers"
|
static let providersContainerName = "Providers"
|
||||||
}
|
}
|
||||||
|
|
||||||
// milliseconds
|
// milliseconds
|
||||||
enum RateLimit {
|
enum RateLimit {
|
||||||
static let providerManager = 10000
|
static let providerManager = 10000
|
||||||
|
|
||||||
static let vpnToggle = 500
|
static let vpnToggle = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Log {
|
enum Log {
|
||||||
enum App {
|
enum App {
|
||||||
static let url = containerURL(filename: "App.log")
|
static let url = containerURL(filename: "App.log")
|
||||||
|
|
||||||
static let format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
|
static let format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Tunnel {
|
enum Tunnel {
|
||||||
static let path = containerPath(filename: "Tunnel.log")
|
static let path = containerPath(filename: "Tunnel.log")
|
||||||
|
|
||||||
static let format = "$DHH:mm:ss$d - $M"
|
static let format = "$DHH:mm:ss$d - $M"
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let parentPath = "Library/Caches"
|
private static let parentPath = "Library/Caches"
|
||||||
|
|
||||||
static let level: SwiftyBeaver.Level = {
|
static let level: SwiftyBeaver.Level = {
|
||||||
|
@ -160,9 +160,9 @@ extension Constants {
|
||||||
}
|
}
|
||||||
return .init(rawValue: levelNum) ?? .info
|
return .init(rawValue: levelNum) ?? .info
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static let maxBytes = 100000
|
static let maxBytes = 100000
|
||||||
|
|
||||||
static let refreshInterval: TimeInterval = 5.0
|
static let refreshInterval: TimeInterval = 5.0
|
||||||
|
|
||||||
private static func containerURL(filename: String) -> URL {
|
private static func containerURL(filename: String) -> URL {
|
||||||
|
@ -175,30 +175,30 @@ extension Constants {
|
||||||
"\(parentPath)/\(filename)"
|
"\(parentPath)/\(filename)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum URLs {
|
enum URLs {
|
||||||
static let readme = Repos.apple.appendingPathComponent("blob/master/README.md")
|
static let readme = Repos.apple.appendingPathComponent("blob/master/README.md")
|
||||||
|
|
||||||
static let changelog = Repos.apple.appendingPathComponent("blob/master/CHANGELOG.md")
|
static let changelog = Repos.apple.appendingPathComponent("blob/master/CHANGELOG.md")
|
||||||
|
|
||||||
static let filetypes: [UTType] = [.item]
|
static let filetypes: [UTType] = [.item]
|
||||||
|
|
||||||
static let website = URL(string: "https://\(Domain.name)")!
|
static let website = URL(string: "https://\(Domain.name)")!
|
||||||
|
|
||||||
static let faq = website.appendingPathComponent("faq")
|
static let faq = website.appendingPathComponent("faq")
|
||||||
|
|
||||||
static let disclaimer = website.appendingPathComponent("disclaimer")
|
static let disclaimer = website.appendingPathComponent("disclaimer")
|
||||||
|
|
||||||
static let privacyPolicy = website.appendingPathComponent("privacy")
|
static let privacyPolicy = website.appendingPathComponent("privacy")
|
||||||
|
|
||||||
static let donate = website.appendingPathComponent("donate")
|
static let donate = website.appendingPathComponent("donate")
|
||||||
|
|
||||||
static let subreddit = URL(string: "https://www.reddit.com/r/passepartout")!
|
static let subreddit = URL(string: "https://www.reddit.com/r/passepartout")!
|
||||||
|
|
||||||
static let twitch = URL(string: "twitch://stream/keeshux")!
|
static let twitch = URL(string: "twitch://stream/keeshux")!
|
||||||
|
|
||||||
static let twitchFallback = URL(string: "https://twitch.tv/keeshux")!
|
static let twitchFallback = URL(string: "https://twitch.tv/keeshux")!
|
||||||
|
|
||||||
static let githubSponsors = URL(string: "https://www.github.com/sponsors/passepartoutvpn")!
|
static let githubSponsors = URL(string: "https://www.github.com/sponsors/passepartoutvpn")!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,31 +206,31 @@ extension Constants {
|
||||||
private static let githubRoot = URL(string: "https://github.com/passepartoutvpn/")!
|
private static let githubRoot = URL(string: "https://github.com/passepartoutvpn/")!
|
||||||
|
|
||||||
private static let githubRawRoot = URL(string: "https://\(Domain.name)/")!
|
private static let githubRawRoot = URL(string: "https://\(Domain.name)/")!
|
||||||
|
|
||||||
private static func github(repo: String) -> URL {
|
private static func github(repo: String) -> URL {
|
||||||
githubRoot.appendingPathComponent(repo)
|
githubRoot.appendingPathComponent(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func githubRaw(repo: String) -> URL {
|
private static func githubRaw(repo: String) -> URL {
|
||||||
githubRawRoot.appendingPathComponent(repo)
|
githubRawRoot.appendingPathComponent(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
static let apple = github(repo: "passepartout-apple")
|
static let apple = github(repo: "passepartout-apple")
|
||||||
|
|
||||||
static let api = githubRaw(repo: "api")
|
static let api = githubRaw(repo: "api")
|
||||||
}
|
}
|
||||||
|
|
||||||
// milliseconds
|
// milliseconds
|
||||||
enum Delays {
|
enum Delays {
|
||||||
static let scrolling = 100
|
static let scrolling = 100
|
||||||
|
|
||||||
// @available(*, deprecated, message: "file importer stops showing again after closing with swipe down")
|
// @available(*, deprecated, message: "file importer stops showing again after closing with swipe down")
|
||||||
static let xxxPresentFileImporter = 200
|
static let xxxPresentFileImporter = 200
|
||||||
|
|
||||||
// @available(*, deprecated, message: "edited shortcut is outdated in delegate")
|
// @available(*, deprecated, message: "edited shortcut is outdated in delegate")
|
||||||
static let xxxReloadEditedShortcut = 200
|
static let xxxReloadEditedShortcut = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Rating {
|
enum Rating {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
static let eventCount = 10
|
static let eventCount = 10
|
||||||
|
|
|
@ -30,7 +30,7 @@ extension View {
|
||||||
var themeIdiom: UIUserInterfaceIdiom {
|
var themeIdiom: UIUserInterfaceIdiom {
|
||||||
UIDevice.current.userInterfaceIdiom
|
UIDevice.current.userInterfaceIdiom
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeIsiPadPortrait: Bool {
|
var themeIsiPadPortrait: Bool {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
false
|
false
|
||||||
|
@ -39,7 +39,7 @@ extension View {
|
||||||
return device.userInterfaceIdiom == .pad && device.orientation.isPortrait
|
return device.userInterfaceIdiom == .pad && device.orientation.isPortrait
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeIsiPadMultitasking: Bool {
|
var themeIsiPadMultitasking: Bool {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
false
|
false
|
||||||
|
@ -128,15 +128,15 @@ extension View {
|
||||||
fileprivate var themePrimaryBackgroundColor: Color {
|
fileprivate var themePrimaryBackgroundColor: Color {
|
||||||
Color(Asset.Assets.primaryColor.color)
|
Color(Asset.Assets.primaryColor.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var themeSecondaryColor: Color {
|
fileprivate var themeSecondaryColor: Color {
|
||||||
.secondary
|
.secondary
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var themeLightTextColor: Color {
|
fileprivate var themeLightTextColor: Color {
|
||||||
Color(Asset.Assets.lightTextColor.color)
|
Color(Asset.Assets.lightTextColor.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var themeErrorColor: Color {
|
fileprivate var themeErrorColor: Color {
|
||||||
.red
|
.red
|
||||||
}
|
}
|
||||||
|
@ -160,11 +160,11 @@ extension View {
|
||||||
var themeAssetsLogoImage: String {
|
var themeAssetsLogoImage: String {
|
||||||
"Logo"
|
"Logo"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeCheckmarkImage: String {
|
var themeCheckmarkImage: String {
|
||||||
"checkmark"
|
"checkmark"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeShareImage: String {
|
var themeShareImage: String {
|
||||||
"square.and.arrow.up"
|
"square.and.arrow.up"
|
||||||
}
|
}
|
||||||
|
@ -172,11 +172,11 @@ extension View {
|
||||||
var themeCopyImage: String {
|
var themeCopyImage: String {
|
||||||
"doc.on.doc"
|
"doc.on.doc"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeCloseImage: String {
|
var themeCloseImage: String {
|
||||||
"xmark"
|
"xmark"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeConceilImage: String {
|
var themeConceilImage: String {
|
||||||
"eye.slash"
|
"eye.slash"
|
||||||
}
|
}
|
||||||
|
@ -186,11 +186,11 @@ extension View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Organizer
|
// MARK: Organizer
|
||||||
|
|
||||||
func themeAssetsProviderImage(_ providerName: ProviderName) -> String {
|
func themeAssetsProviderImage(_ providerName: ProviderName) -> String {
|
||||||
"providers/\(providerName)"
|
"providers/\(providerName)"
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeAssetsCountryImage(_ countryCode: String) -> String {
|
func themeAssetsCountryImage(_ countryCode: String) -> String {
|
||||||
"flags/\(countryCode.lowercased())"
|
"flags/\(countryCode.lowercased())"
|
||||||
}
|
}
|
||||||
|
@ -198,15 +198,15 @@ extension View {
|
||||||
var themeProviderImage: String {
|
var themeProviderImage: String {
|
||||||
"externaldrive.connected.to.line.below"
|
"externaldrive.connected.to.line.below"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeHostFilesImage: String {
|
var themeHostFilesImage: String {
|
||||||
"folder"
|
"folder"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeHostTextImage: String {
|
var themeHostTextImage: String {
|
||||||
"text.justify"
|
"text.justify"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeSettingsImage: String {
|
var themeSettingsImage: String {
|
||||||
"gearshape"
|
"gearshape"
|
||||||
}
|
}
|
||||||
|
@ -222,11 +222,11 @@ extension View {
|
||||||
var themeWriteReviewImage: String {
|
var themeWriteReviewImage: String {
|
||||||
"star"
|
"star"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeAddMenuImage: String {
|
var themeAddMenuImage: String {
|
||||||
"plus"
|
"plus"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeProfileActiveImage: String {
|
var themeProfileActiveImage: String {
|
||||||
"checkmark.circle"
|
"checkmark.circle"
|
||||||
}
|
}
|
||||||
|
@ -257,19 +257,19 @@ extension View {
|
||||||
"highlighter"
|
"highlighter"
|
||||||
// "character.cursor.ibeam"
|
// "character.cursor.ibeam"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeDuplicateImage: String {
|
var themeDuplicateImage: String {
|
||||||
"doc.on.doc"
|
"doc.on.doc"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeUninstallImage: String {
|
var themeUninstallImage: String {
|
||||||
"arrow.uturn.down"
|
"arrow.uturn.down"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeDeleteImage: String {
|
var themeDeleteImage: String {
|
||||||
"trash"
|
"trash"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeVPNProtocolImage: String {
|
var themeVPNProtocolImage: String {
|
||||||
"bolt"
|
"bolt"
|
||||||
// "waveform.path.ecg"
|
// "waveform.path.ecg"
|
||||||
|
@ -277,36 +277,36 @@ extension View {
|
||||||
// "pc"
|
// "pc"
|
||||||
// "captions.bubble.fill"
|
// "captions.bubble.fill"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeEndpointImage: String {
|
var themeEndpointImage: String {
|
||||||
"link"
|
"link"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeAccountImage: String {
|
var themeAccountImage: String {
|
||||||
"person"
|
"person"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeProviderLocationImage: String {
|
var themeProviderLocationImage: String {
|
||||||
"location"
|
"location"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeProviderPresetImage: String {
|
var themeProviderPresetImage: String {
|
||||||
"slider.horizontal.3"
|
"slider.horizontal.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeNetworkSettingsImage: String {
|
var themeNetworkSettingsImage: String {
|
||||||
// "network"
|
// "network"
|
||||||
"globe"
|
"globe"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeOnDemandImage: String {
|
var themeOnDemandImage: String {
|
||||||
"wifi"
|
"wifi"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeDiagnosticsImage: String {
|
var themeDiagnosticsImage: String {
|
||||||
"bandage.fill"
|
"bandage.fill"
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeFAQImage: String {
|
var themeFAQImage: String {
|
||||||
"questionmark.diamond"
|
"questionmark.diamond"
|
||||||
}
|
}
|
||||||
|
@ -345,11 +345,11 @@ extension View {
|
||||||
func themeSecondaryTextStyle() -> some View {
|
func themeSecondaryTextStyle() -> some View {
|
||||||
foregroundColor(themeSecondaryColor)
|
foregroundColor(themeSecondaryColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeLightTextStyle() -> some View {
|
func themeLightTextStyle() -> some View {
|
||||||
foregroundColor(themeLightTextColor)
|
foregroundColor(themeLightTextColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 15, *)
|
@available(iOS 15, *)
|
||||||
func themePrimaryTintStyle() -> some View {
|
func themePrimaryTintStyle() -> some View {
|
||||||
tint(themePrimaryBackgroundColor)
|
tint(themePrimaryBackgroundColor)
|
||||||
|
@ -363,7 +363,7 @@ extension View {
|
||||||
lineLimit(1)
|
lineLimit(1)
|
||||||
.truncationMode(.middle)
|
.truncationMode(.middle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeRawTextStyle() -> some View {
|
func themeRawTextStyle() -> some View {
|
||||||
disableAutocorrection(true)
|
disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
@ -381,7 +381,7 @@ extension View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Animations
|
// MARK: Animations
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func themeAnimation<V: Equatable>(on value: V) -> some View {
|
func themeAnimation<V: Equatable>(on value: V) -> some View {
|
||||||
animation(.default, value: value)
|
animation(.default, value: value)
|
||||||
|
@ -416,7 +416,7 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeSaveButtonLabel() -> some View {
|
func themeSaveButtonLabel() -> some View {
|
||||||
// themeCheckmarkImage.asSystemImage
|
// themeCheckmarkImage.asSystemImage
|
||||||
Text(L10n.Global.Strings.save)
|
Text(L10n.Global.Strings.save)
|
||||||
|
@ -451,7 +451,7 @@ extension View {
|
||||||
.foregroundColor(themeSecondaryColor)
|
.foregroundColor(themeSecondaryColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func themeErrorMessage(_ message: String?) -> some View {
|
func themeErrorMessage(_ message: String?) -> some View {
|
||||||
if let message = message {
|
if let message = message {
|
||||||
|
@ -480,13 +480,13 @@ extension View {
|
||||||
.keyboardType(.asciiCapable)
|
.keyboardType(.asciiCapable)
|
||||||
.themeRawTextStyle()
|
.themeRawTextStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeValidIPAddress(_ ipAddress: String?) -> some View {
|
func themeValidIPAddress(_ ipAddress: String?) -> some View {
|
||||||
themeValidating(ipAddress, validator: Validators.ipAddress)
|
themeValidating(ipAddress, validator: Validators.ipAddress)
|
||||||
.keyboardType(.numbersAndPunctuation)
|
.keyboardType(.numbersAndPunctuation)
|
||||||
.themeRawTextStyle()
|
.themeRawTextStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
func themeValidSocketPort() -> some View {
|
func themeValidSocketPort() -> some View {
|
||||||
keyboardType(.numberPad)
|
keyboardType(.numberPad)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,20 +30,20 @@ import PassepartoutLibrary
|
||||||
@MainActor
|
@MainActor
|
||||||
class AppContext {
|
class AppContext {
|
||||||
let logManager: LogManager
|
let logManager: LogManager
|
||||||
|
|
||||||
let productManager: ProductManager
|
let productManager: ProductManager
|
||||||
|
|
||||||
private let reviewer: Reviewer
|
private let reviewer: Reviewer
|
||||||
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
init(coreContext: CoreContext) {
|
init(coreContext: CoreContext) {
|
||||||
logManager = LogManager(logFile: Constants.Log.App.url)
|
logManager = LogManager(logFile: Constants.Log.App.url)
|
||||||
logManager.logLevel = Constants.Log.level
|
logManager.logLevel = Constants.Log.level
|
||||||
logManager.logFormat = Constants.Log.App.format
|
logManager.logFormat = Constants.Log.App.format
|
||||||
logManager.configureLogging()
|
logManager.configureLogging()
|
||||||
pp_log.info("Logging to: \(logManager.logFile!)")
|
pp_log.info("Logging to: \(logManager.logFile!)")
|
||||||
|
|
||||||
productManager = ProductManager(
|
productManager = ProductManager(
|
||||||
appType: Constants.InApp.appType,
|
appType: Constants.InApp.appType,
|
||||||
buildProducts: Constants.InApp.buildProducts
|
buildProducts: Constants.InApp.buildProducts
|
||||||
|
@ -51,12 +51,12 @@ class AppContext {
|
||||||
|
|
||||||
reviewer = Reviewer()
|
reviewer = Reviewer()
|
||||||
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
|
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
|
||||||
|
|
||||||
// post
|
// post
|
||||||
|
|
||||||
configureObjects(coreContext: coreContext)
|
configureObjects(coreContext: coreContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureObjects(coreContext: CoreContext) {
|
private func configureObjects(coreContext: CoreContext) {
|
||||||
coreContext.vpnManager.isOnDemandRulesSupported = {
|
coreContext.vpnManager.isOnDemandRulesSupported = {
|
||||||
self.isEligibleForOnDemandRules()
|
self.isEligibleForOnDemandRules()
|
||||||
|
@ -81,7 +81,7 @@ class AppContext {
|
||||||
}
|
}
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eligibility: ignore network settings if ineligible
|
// eligibility: ignore network settings if ineligible
|
||||||
private func isEligibleForNetworkSettings() -> Bool {
|
private func isEligibleForNetworkSettings() -> Bool {
|
||||||
guard productManager.isEligible(forFeature: .networkSettings) else {
|
guard productManager.isEligible(forFeature: .networkSettings) else {
|
||||||
|
@ -90,7 +90,7 @@ class AppContext {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// eligibility: reset on-demand rules if no trusted networks
|
// eligibility: reset on-demand rules if no trusted networks
|
||||||
private func isEligibleForOnDemandRules() -> Bool {
|
private func isEligibleForOnDemandRules() -> Bool {
|
||||||
guard productManager.isEligible(forFeature: .trustedNetworks) else {
|
guard productManager.isEligible(forFeature: .trustedNetworks) else {
|
||||||
|
|
|
@ -38,7 +38,7 @@ extension ProviderMetadata: Identifiable, Comparable, Hashable {
|
||||||
public static func <(lhs: Self, rhs: Self) -> Bool {
|
public static func <(lhs: Self, rhs: Self) -> Bool {
|
||||||
lhs.fullName.lowercased() < rhs.fullName.lowercased()
|
lhs.fullName.lowercased() < rhs.fullName.lowercased()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(name)
|
hasher.combine(name)
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ extension ProviderServer: Comparable {
|
||||||
public static func ==(lhs: Self, rhs: Self) -> Bool {
|
public static func ==(lhs: Self, rhs: Self) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Default" comes first (nil localizedName)
|
// "Default" comes first (nil localizedName)
|
||||||
public static func <(lhs: Self, rhs: Self) -> Bool {
|
public static func <(lhs: Self, rhs: Self) -> Bool {
|
||||||
guard lhs.localizedName != rhs.localizedName else {
|
guard lhs.localizedName != rhs.localizedName else {
|
||||||
|
@ -113,7 +113,7 @@ extension ProviderMetadata {
|
||||||
}
|
}
|
||||||
return URL(string: string)
|
return URL(string: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
var referralURL: URL? {
|
var referralURL: URL? {
|
||||||
guard let string = Constants.URLs.referrals[name] else {
|
guard let string = Constants.URLs.referrals[name] else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -28,7 +28,7 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
protocol ProviderProfileAvailability {
|
protocol ProviderProfileAvailability {
|
||||||
var profile: Profile { get }
|
var profile: Profile { get }
|
||||||
|
|
||||||
var providerManager: ProviderManager { get }
|
var providerManager: ProviderManager { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,12 +31,12 @@ extension VPNProtocolType {
|
||||||
let protos: [Self] = [.openVPN, .wireGuard]
|
let protos: [Self] = [.openVPN, .wireGuard]
|
||||||
return protos.map(\.fileExtension)
|
return protos.map(\.fileExtension)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var fileExtension: String {
|
var fileExtension: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .openVPN:
|
case .openVPN:
|
||||||
return "ovpn"
|
return "ovpn"
|
||||||
|
|
||||||
case .wireGuard:
|
case .wireGuard:
|
||||||
return "conf"
|
return "conf"
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,11 +27,11 @@ import Foundation
|
||||||
|
|
||||||
struct BuildProducts {
|
struct BuildProducts {
|
||||||
private let productsAtBuild: (Int) -> [LocalProduct]
|
private let productsAtBuild: (Int) -> [LocalProduct]
|
||||||
|
|
||||||
init(productsAtBuild: @escaping (Int) -> [LocalProduct]) {
|
init(productsAtBuild: @escaping (Int) -> [LocalProduct]) {
|
||||||
self.productsAtBuild = productsAtBuild
|
self.productsAtBuild = productsAtBuild
|
||||||
}
|
}
|
||||||
|
|
||||||
func products(atBuild build: Int) -> [LocalProduct] {
|
func products(atBuild build: Int) -> [LocalProduct] {
|
||||||
productsAtBuild(build)
|
productsAtBuild(build)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,13 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
|
||||||
private static let bundle = "com.algoritmico.\(bundleSubdomain).Passepartout"
|
private static let bundle = "com.algoritmico.\(bundleSubdomain).Passepartout"
|
||||||
|
|
||||||
private static let donationsBundle = "\(bundle).donations"
|
private static let donationsBundle = "\(bundle).donations"
|
||||||
|
|
||||||
private static let featuresBundle = "\(bundle).features"
|
private static let featuresBundle = "\(bundle).features"
|
||||||
|
|
||||||
static let providersBundle = "\(bundle).providers"
|
static let providersBundle = "\(bundle).providers"
|
||||||
|
|
||||||
// MARK: Donations
|
// MARK: Donations
|
||||||
|
|
||||||
static let tinyDonation = LocalProduct(donationDescription: "Tiny")
|
static let tinyDonation = LocalProduct(donationDescription: "Tiny")
|
||||||
|
|
||||||
static let smallDonation = LocalProduct(donationDescription: "Small")
|
static let smallDonation = LocalProduct(donationDescription: "Small")
|
||||||
|
@ -51,7 +51,7 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
|
||||||
static let hugeDonation = LocalProduct(donationDescription: "Huge")
|
static let hugeDonation = LocalProduct(donationDescription: "Huge")
|
||||||
|
|
||||||
static let maxiDonation = LocalProduct(donationDescription: "Maxi")
|
static let maxiDonation = LocalProduct(donationDescription: "Maxi")
|
||||||
|
|
||||||
static let allDonations: [LocalProduct] = [
|
static let allDonations: [LocalProduct] = [
|
||||||
.tinyDonation,
|
.tinyDonation,
|
||||||
.smallDonation,
|
.smallDonation,
|
||||||
|
@ -66,9 +66,9 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Features
|
// MARK: Features
|
||||||
|
|
||||||
static let allProviders = LocalProduct(featureId: "all_providers")
|
static let allProviders = LocalProduct(featureId: "all_providers")
|
||||||
|
|
||||||
static let networkSettings = LocalProduct(featureId: "network_settings")
|
static let networkSettings = LocalProduct(featureId: "network_settings")
|
||||||
|
|
||||||
static let trustedNetworks = LocalProduct(featureId: "trusted_networks")
|
static let trustedNetworks = LocalProduct(featureId: "trusted_networks")
|
||||||
|
@ -76,7 +76,7 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
|
||||||
static let siriShortcuts = LocalProduct(featureId: "siri")
|
static let siriShortcuts = LocalProduct(featureId: "siri")
|
||||||
|
|
||||||
static let fullVersion_iOS = LocalProduct(featureId: "full_version")
|
static let fullVersion_iOS = LocalProduct(featureId: "full_version")
|
||||||
|
|
||||||
static let fullVersion_macOS = LocalProduct(featureId: "full_mac_version")
|
static let fullVersion_macOS = LocalProduct(featureId: "full_mac_version")
|
||||||
|
|
||||||
static let fullVersion = LocalProduct(featureId: "full_multi_version")
|
static let fullVersion = LocalProduct(featureId: "full_multi_version")
|
||||||
|
@ -100,7 +100,7 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
|
||||||
static var all: [LocalProduct] {
|
static var all: [LocalProduct] {
|
||||||
allDonations + allFeatures// + allProviders
|
allDonations + allFeatures// + allProviders
|
||||||
}
|
}
|
||||||
|
|
||||||
var isDonation: Bool {
|
var isDonation: Bool {
|
||||||
rawValue.hasPrefix(LocalProduct.donationsBundle)
|
rawValue.hasPrefix(LocalProduct.donationsBundle)
|
||||||
}
|
}
|
||||||
|
@ -112,11 +112,11 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable {
|
||||||
var isProvider: Bool {
|
var isProvider: Bool {
|
||||||
rawValue.hasPrefix(LocalProduct.providersBundle)
|
rawValue.hasPrefix(LocalProduct.providersBundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: RawRepresentable
|
// MARK: RawRepresentable
|
||||||
|
|
||||||
let rawValue: String
|
let rawValue: String
|
||||||
|
|
||||||
init?(rawValue: String) {
|
init?(rawValue: String) {
|
||||||
self.rawValue = rawValue
|
self.rawValue = rawValue
|
||||||
}
|
}
|
||||||
|
@ -140,22 +140,22 @@ private extension ProviderName {
|
||||||
switch self {
|
switch self {
|
||||||
case .mullvad:
|
case .mullvad:
|
||||||
return "Mullvad"
|
return "Mullvad"
|
||||||
|
|
||||||
case .nordvpn:
|
case .nordvpn:
|
||||||
return "NordVPN"
|
return "NordVPN"
|
||||||
|
|
||||||
case .pia:
|
case .pia:
|
||||||
return "PIA"
|
return "PIA"
|
||||||
|
|
||||||
case .protonvpn:
|
case .protonvpn:
|
||||||
return "ProtonVPN"
|
return "ProtonVPN"
|
||||||
|
|
||||||
case .tunnelbear:
|
case .tunnelbear:
|
||||||
return "TunnelBear"
|
return "TunnelBear"
|
||||||
|
|
||||||
case .vyprvpn:
|
case .vyprvpn:
|
||||||
return "VyprVPN"
|
return "VyprVPN"
|
||||||
|
|
||||||
case .windscribe:
|
case .windscribe:
|
||||||
return "Windscribe"
|
return "Windscribe"
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ import Combine
|
||||||
|
|
||||||
enum ProductError: Error {
|
enum ProductError: Error {
|
||||||
case uneligible
|
case uneligible
|
||||||
|
|
||||||
case beta
|
case beta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,30 +40,30 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
case freemium = 0
|
case freemium = 0
|
||||||
|
|
||||||
case beta = 1
|
case beta = 1
|
||||||
|
|
||||||
case fullVersion = 2
|
case fullVersion = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
let appType: AppType
|
let appType: AppType
|
||||||
|
|
||||||
let buildProducts: BuildProducts
|
let buildProducts: BuildProducts
|
||||||
|
|
||||||
let didRefundProducts = PassthroughSubject<Void, Never>()
|
let didRefundProducts = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
@Published private(set) var isRefreshingProducts = false
|
@Published private(set) var isRefreshingProducts = false
|
||||||
|
|
||||||
@Published private(set) var products: [SKProduct]
|
@Published private(set) var products: [SKProduct]
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
private let inApp: InApp<LocalProduct>
|
private let inApp: InApp<LocalProduct>
|
||||||
|
|
||||||
private var purchasedAppBuild: Int?
|
private var purchasedAppBuild: Int?
|
||||||
|
|
||||||
private var purchasedFeatures: Set<LocalProduct>
|
private var purchasedFeatures: Set<LocalProduct>
|
||||||
|
|
||||||
private var purchaseDates: [LocalProduct: Date]
|
private var purchaseDates: [LocalProduct: Date]
|
||||||
|
|
||||||
private var cancelledPurchases: Set<LocalProduct>? {
|
private var cancelledPurchases: Set<LocalProduct>? {
|
||||||
willSet {
|
willSet {
|
||||||
guard cancelledPurchases != nil else {
|
guard cancelledPurchases != nil else {
|
||||||
|
@ -76,20 +76,20 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
detectRefunds(newCancelledPurchases)
|
detectRefunds(newCancelledPurchases)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var refreshRequest: SKReceiptRefreshRequest?
|
private var refreshRequest: SKReceiptRefreshRequest?
|
||||||
|
|
||||||
init(appType: AppType, buildProducts: BuildProducts) {
|
init(appType: AppType, buildProducts: BuildProducts) {
|
||||||
self.appType = appType
|
self.appType = appType
|
||||||
self.buildProducts = buildProducts
|
self.buildProducts = buildProducts
|
||||||
|
|
||||||
products = []
|
products = []
|
||||||
inApp = InApp()
|
inApp = InApp()
|
||||||
purchasedAppBuild = nil
|
purchasedAppBuild = nil
|
||||||
purchasedFeatures = []
|
purchasedFeatures = []
|
||||||
purchaseDates = [:]
|
purchaseDates = [:]
|
||||||
cancelledPurchases = nil
|
cancelledPurchases = nil
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
reloadReceipt()
|
reloadReceipt()
|
||||||
|
@ -97,15 +97,15 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
refreshProducts()
|
refreshProducts()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
SKPaymentQueue.default().remove(self)
|
SKPaymentQueue.default().remove(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func canMakePayments() -> Bool {
|
func canMakePayments() -> Bool {
|
||||||
SKPaymentQueue.canMakePayments()
|
SKPaymentQueue.canMakePayments()
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshProducts() {
|
func refreshProducts() {
|
||||||
let ids = LocalProduct.all
|
let ids = LocalProduct.all
|
||||||
guard !ids.isEmpty else {
|
guard !ids.isEmpty else {
|
||||||
|
@ -130,7 +130,7 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
func product(withIdentifier identifier: LocalProduct) -> SKProduct? {
|
func product(withIdentifier identifier: LocalProduct) -> SKProduct? {
|
||||||
inApp.product(withIdentifier: identifier)
|
inApp.product(withIdentifier: identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func featureProducts(including: [LocalProduct]) -> [SKProduct] {
|
func featureProducts(including: [LocalProduct]) -> [SKProduct] {
|
||||||
inApp.products.filter {
|
inApp.products.filter {
|
||||||
guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
|
guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
|
||||||
|
@ -145,7 +145,7 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func featureProducts(excluding: [LocalProduct]) -> [SKProduct] {
|
func featureProducts(excluding: [LocalProduct]) -> [SKProduct] {
|
||||||
inApp.products.filter {
|
inApp.products.filter {
|
||||||
guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
|
guard let p = LocalProduct(rawValue: $0.productIdentifier) else {
|
||||||
|
@ -160,7 +160,7 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func purchase(_ product: SKProduct, completionHandler: @escaping (Result<InAppPurchaseResult, Error>) -> Void) {
|
func purchase(_ product: SKProduct, completionHandler: @escaping (Result<InAppPurchaseResult, Error>) -> Void) {
|
||||||
inApp.purchase(product: product) { result in
|
inApp.purchase(product: product) { result in
|
||||||
if case .success = result {
|
if case .success = result {
|
||||||
|
@ -171,7 +171,7 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restorePurchases(completionHandler: @escaping (Error?) -> Void) {
|
func restorePurchases(completionHandler: @escaping (Error?) -> Void) {
|
||||||
inApp.restorePurchases { (finished, _, error) in
|
inApp.restorePurchases { (finished, _, error) in
|
||||||
guard finished else {
|
guard finished else {
|
||||||
|
@ -184,7 +184,7 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: In-app eligibility
|
// MARK: In-app eligibility
|
||||||
|
|
||||||
private func isCurrentPlatformVersion() -> Bool {
|
private func isCurrentPlatformVersion() -> Bool {
|
||||||
purchasedFeatures.contains(isMac ? .fullVersion_macOS : .fullVersion_iOS)
|
purchasedFeatures.contains(isMac ? .fullVersion_macOS : .fullVersion_iOS)
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,7 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
func isEligibleForFeedback() -> Bool {
|
func isEligibleForFeedback() -> Bool {
|
||||||
appType == .beta || !purchasedFeatures.isEmpty
|
appType == .beta || !purchasedFeatures.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasPurchased(_ product: LocalProduct) -> Bool {
|
func hasPurchased(_ product: LocalProduct) -> Bool {
|
||||||
purchasedFeatures.contains(product)
|
purchasedFeatures.contains(product)
|
||||||
}
|
}
|
||||||
|
@ -257,7 +257,7 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
if let iapReceipts = receipt.inAppPurchaseReceipts {
|
if let iapReceipts = receipt.inAppPurchaseReceipts {
|
||||||
purchaseDates.removeAll()
|
purchaseDates.removeAll()
|
||||||
|
|
||||||
pp_log.debug("In-app receipts:")
|
pp_log.debug("In-app receipts:")
|
||||||
iapReceipts.forEach {
|
iapReceipts.forEach {
|
||||||
guard let pid = $0.productIdentifier, let product = LocalProduct(rawValue: pid) else {
|
guard let pid = $0.productIdentifier, let product = LocalProduct(rawValue: pid) else {
|
||||||
|
@ -296,7 +296,7 @@ extension ProductManager {
|
||||||
let isEligibleForFullVersion = isFullVersion()
|
let isEligibleForFullVersion = isFullVersion()
|
||||||
let hasCancelledFullVersion: Bool
|
let hasCancelledFullVersion: Bool
|
||||||
let hasCancelledTrustedNetworks: Bool
|
let hasCancelledTrustedNetworks: Bool
|
||||||
|
|
||||||
if isMac {
|
if isMac {
|
||||||
hasCancelledFullVersion = !isEligibleForFullVersion && (
|
hasCancelledFullVersion = !isEligibleForFullVersion && (
|
||||||
refunds.contains(.fullVersion) || refunds.contains(.fullVersion_macOS)
|
refunds.contains(.fullVersion) || refunds.contains(.fullVersion_macOS)
|
||||||
|
@ -308,7 +308,7 @@ extension ProductManager {
|
||||||
)
|
)
|
||||||
hasCancelledTrustedNetworks = !isEligibleForFullVersion && refunds.contains(.trustedNetworks)
|
hasCancelledTrustedNetworks = !isEligibleForFullVersion && refunds.contains(.trustedNetworks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// review features and potentially revert them if they were used (Siri is handled in AppDelegate)
|
// review features and potentially revert them if they were used (Siri is handled in AppDelegate)
|
||||||
if hasCancelledFullVersion || hasCancelledTrustedNetworks {
|
if hasCancelledFullVersion || hasCancelledTrustedNetworks {
|
||||||
didRefundProducts.send()
|
didRefundProducts.send()
|
||||||
|
|
|
@ -36,10 +36,10 @@ extension IntentDispatcher {
|
||||||
|
|
||||||
case activeAndConnected(UUID)
|
case activeAndConnected(UUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias VPNIntentActivity = IntentActivity<VPNManager>
|
typealias VPNIntentActivity = IntentActivity<VPNManager>
|
||||||
|
|
||||||
static let enableVPN = VPNIntentActivity(name: Constants.Activities.enableVPN) { activity, vpnManager in
|
static let enableVPN = VPNIntentActivity(name: Constants.Activities.enableVPN) { _, vpnManager in
|
||||||
pp_log.info("Enabling VPN...")
|
pp_log.info("Enabling VPN...")
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
@ -51,7 +51,7 @@ extension IntentDispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let disableVPN = VPNIntentActivity(name: Constants.Activities.disableVPN) { activity, vpnManager in
|
static let disableVPN = VPNIntentActivity(name: Constants.Activities.disableVPN) { _, vpnManager in
|
||||||
pp_log.info("Disabling VPN...")
|
pp_log.info("Disabling VPN...")
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
@ -81,7 +81,7 @@ extension IntentDispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let moveToLocation = VPNIntentActivity(name: Constants.Activities.moveToLocation) { activity, vpnManager in
|
static let moveToLocation = VPNIntentActivity(name: Constants.Activities.moveToLocation) { activity, vpnManager in
|
||||||
pp_log.info("Moving to VPN location...")
|
pp_log.info("Moving to VPN location...")
|
||||||
|
|
||||||
|
@ -110,27 +110,27 @@ extension IntentDispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let trustCellularNetwork = VPNIntentActivity(name: Constants.Activities.trustCellularNetwork) { activity, vpnManager in
|
static let trustCellularNetwork = VPNIntentActivity(name: Constants.Activities.trustCellularNetwork) { _, vpnManager in
|
||||||
pp_log.info("Trusting mobile network...")
|
pp_log.info("Trusting mobile network...")
|
||||||
handleCellularNetwork(true, vpnManager)
|
handleCellularNetwork(true, vpnManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
static let trustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.trustCurrentNetwork) { activity, vpnManager in
|
static let trustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.trustCurrentNetwork) { _, vpnManager in
|
||||||
pp_log.info("Trusting current Wi-Fi...")
|
pp_log.info("Trusting current Wi-Fi...")
|
||||||
handleCurrentNetwork(true, vpnManager)
|
handleCurrentNetwork(true, vpnManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
static let untrustCellularNetwork = VPNIntentActivity(name: Constants.Activities.untrustCellularNetwork) { activity, vpnManager in
|
static let untrustCellularNetwork = VPNIntentActivity(name: Constants.Activities.untrustCellularNetwork) { _, vpnManager in
|
||||||
pp_log.info("Untrusting mobile network...")
|
pp_log.info("Untrusting mobile network...")
|
||||||
handleCellularNetwork(false, vpnManager)
|
handleCellularNetwork(false, vpnManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
static let untrustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.untrustCurrentNetwork) { activity, vpnManager in
|
static let untrustCurrentNetwork = VPNIntentActivity(name: Constants.Activities.untrustCurrentNetwork) { _, vpnManager in
|
||||||
pp_log.info("Untrusting current Wi-Fi...")
|
pp_log.info("Untrusting current Wi-Fi...")
|
||||||
handleCurrentNetwork(false, vpnManager)
|
handleCurrentNetwork(false, vpnManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleCellularNetwork(_ trust: Bool, _ vpnManager: VPNManager) {
|
private static func handleCellularNetwork(_ trust: Bool, _ vpnManager: VPNManager) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -30,19 +30,19 @@ import PassepartoutLibrary
|
||||||
class IntentDispatcher {
|
class IntentDispatcher {
|
||||||
private struct Groups {
|
private struct Groups {
|
||||||
static let vpn = "VPN"
|
static let vpn = "VPN"
|
||||||
|
|
||||||
static let trust = "Trust"
|
static let trust = "Trust"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Intents
|
// MARK: Intents
|
||||||
|
|
||||||
static func intentConnect(header: Profile.Header) -> ConnectVPNIntent {
|
static func intentConnect(header: Profile.Header) -> ConnectVPNIntent {
|
||||||
let intent = ConnectVPNIntent()
|
let intent = ConnectVPNIntent()
|
||||||
intent.profileId = header.id.uuidString
|
intent.profileId = header.id.uuidString
|
||||||
intent.profileName = header.name
|
intent.profileName = header.name
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
|
|
||||||
static func intentMoveTo(header: Profile.Header, providerFullName: String, server: ProviderServer) -> MoveToLocationIntent {
|
static func intentMoveTo(header: Profile.Header, providerFullName: String, server: ProviderServer) -> MoveToLocationIntent {
|
||||||
let intent = MoveToLocationIntent()
|
let intent = MoveToLocationIntent()
|
||||||
intent.profileId = header.id.uuidString
|
intent.profileId = header.id.uuidString
|
||||||
|
@ -51,33 +51,33 @@ class IntentDispatcher {
|
||||||
intent.serverName = server.localizedLongDescription(withCategory: false)
|
intent.serverName = server.localizedLongDescription(withCategory: false)
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
|
|
||||||
static func intentEnable() -> EnableVPNIntent {
|
static func intentEnable() -> EnableVPNIntent {
|
||||||
EnableVPNIntent()
|
EnableVPNIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func intentDisable() -> DisableVPNIntent {
|
static func intentDisable() -> DisableVPNIntent {
|
||||||
DisableVPNIntent()
|
DisableVPNIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func intentTrustWiFi() -> TrustCurrentNetworkIntent {
|
static func intentTrustWiFi() -> TrustCurrentNetworkIntent {
|
||||||
TrustCurrentNetworkIntent()
|
TrustCurrentNetworkIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func intentUntrustWiFi() -> UntrustCurrentNetworkIntent {
|
static func intentUntrustWiFi() -> UntrustCurrentNetworkIntent {
|
||||||
UntrustCurrentNetworkIntent()
|
UntrustCurrentNetworkIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func intentTrustCellular() -> TrustCellularNetworkIntent {
|
static func intentTrustCellular() -> TrustCellularNetworkIntent {
|
||||||
TrustCellularNetworkIntent()
|
TrustCellularNetworkIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func intentUntrustCellular() -> UntrustCellularNetworkIntent {
|
static func intentUntrustCellular() -> UntrustCellularNetworkIntent {
|
||||||
UntrustCellularNetworkIntent()
|
UntrustCellularNetworkIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Donations
|
// MARK: Donations
|
||||||
|
|
||||||
static func donateConnection(with profile: Profile, providerManager: ProviderManager) {
|
static func donateConnection(with profile: Profile, providerManager: ProviderManager) {
|
||||||
let genericIntent: INIntent
|
let genericIntent: INIntent
|
||||||
if let providerName = profile.header.providerName {
|
if let providerName = profile.header.providerName {
|
||||||
|
@ -93,24 +93,24 @@ class IntentDispatcher {
|
||||||
} else {
|
} else {
|
||||||
genericIntent = intentConnect(header: profile.header)
|
genericIntent = intentConnect(header: profile.header)
|
||||||
}
|
}
|
||||||
|
|
||||||
let interaction = INInteraction(intent: genericIntent, response: nil)
|
let interaction = INInteraction(intent: genericIntent, response: nil)
|
||||||
interaction.groupIdentifier = profile.id.uuidString
|
interaction.groupIdentifier = profile.id.uuidString
|
||||||
interaction.donateAndLog()
|
interaction.donateAndLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func donateEnableVPN() {
|
static func donateEnableVPN() {
|
||||||
let interaction = INInteraction(intent: intentEnable(), response: nil)
|
let interaction = INInteraction(intent: intentEnable(), response: nil)
|
||||||
interaction.groupIdentifier = Groups.vpn
|
interaction.groupIdentifier = Groups.vpn
|
||||||
interaction.donateAndLog()
|
interaction.donateAndLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func donateDisableVPN() {
|
static func donateDisableVPN() {
|
||||||
let interaction = INInteraction(intent: intentDisable(), response: nil)
|
let interaction = INInteraction(intent: intentDisable(), response: nil)
|
||||||
interaction.groupIdentifier = Groups.vpn
|
interaction.groupIdentifier = Groups.vpn
|
||||||
interaction.donateAndLog()
|
interaction.donateAndLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func donateTrustCurrentNetwork() {
|
static func donateTrustCurrentNetwork() {
|
||||||
let interaction = INInteraction(intent: intentTrustWiFi(), response: nil)
|
let interaction = INInteraction(intent: intentTrustWiFi(), response: nil)
|
||||||
interaction.groupIdentifier = Groups.trust
|
interaction.groupIdentifier = Groups.trust
|
||||||
|
@ -122,19 +122,19 @@ class IntentDispatcher {
|
||||||
interaction.groupIdentifier = Groups.trust
|
interaction.groupIdentifier = Groups.trust
|
||||||
interaction.donateAndLog()
|
interaction.donateAndLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func donateTrustCellularNetwork() {
|
static func donateTrustCellularNetwork() {
|
||||||
let interaction = INInteraction(intent: intentTrustCellular(), response: nil)
|
let interaction = INInteraction(intent: intentTrustCellular(), response: nil)
|
||||||
interaction.groupIdentifier = Groups.trust
|
interaction.groupIdentifier = Groups.trust
|
||||||
interaction.donateAndLog()
|
interaction.donateAndLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func donateUntrustCellularNetwork() {
|
static func donateUntrustCellularNetwork() {
|
||||||
let interaction = INInteraction(intent: intentUntrustCellular(), response: nil)
|
let interaction = INInteraction(intent: intentUntrustCellular(), response: nil)
|
||||||
interaction.groupIdentifier = Groups.trust
|
interaction.groupIdentifier = Groups.trust
|
||||||
interaction.donateAndLog()
|
interaction.donateAndLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func forgetProfile(withHeader header: Profile.Header) {
|
static func forgetProfile(withHeader header: Profile.Header) {
|
||||||
INInteraction.delete(with: header.id.uuidString) { (error) in
|
INInteraction.delete(with: header.id.uuidString) { (error) in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
|
|
|
@ -32,20 +32,20 @@ import PassepartoutLibrary
|
||||||
@MainActor
|
@MainActor
|
||||||
class IntentsManager: NSObject, ObservableObject {
|
class IntentsManager: NSObject, ObservableObject {
|
||||||
@Published private(set) var isReloadingShortcuts = false
|
@Published private(set) var isReloadingShortcuts = false
|
||||||
|
|
||||||
@Published private(set) var shortcuts: [UUID: Shortcut] = [:]
|
@Published private(set) var shortcuts: [UUID: Shortcut] = [:]
|
||||||
|
|
||||||
let shouldDismissIntentView = PassthroughSubject<Void, Never>()
|
let shouldDismissIntentView = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
private var continuation: CheckedContinuation<[INVoiceShortcut], Never>?
|
private var continuation: CheckedContinuation<[INVoiceShortcut], Never>?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
Task {
|
Task {
|
||||||
await reloadShortcuts()
|
await reloadShortcuts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadShortcuts() async {
|
func reloadShortcuts() async {
|
||||||
isReloadingShortcuts = true
|
isReloadingShortcuts = true
|
||||||
do {
|
do {
|
||||||
|
@ -81,10 +81,10 @@ extension IntentsManager: INUIEditVoiceShortcutViewControllerDelegate {
|
||||||
guard let vs = voiceShortcut else {
|
guard let vs = voiceShortcut else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcuts[vs.identifier] = Shortcut(vs)
|
shortcuts[vs.identifier] = Shortcut(vs)
|
||||||
shouldDismissIntentView.send()
|
shouldDismissIntentView.send()
|
||||||
|
|
||||||
// XXX: iOS bug, vs.invocationPhrase here is still the old one before edit
|
// XXX: iOS bug, vs.invocationPhrase here is still the old one before edit
|
||||||
//
|
//
|
||||||
// additionally, back from edit view controller does not trigger either onAppear or
|
// additionally, back from edit view controller does not trigger either onAppear or
|
||||||
|
|
|
@ -27,11 +27,11 @@ import Foundation
|
||||||
|
|
||||||
class MacBundle {
|
class MacBundle {
|
||||||
static let shared = MacBundle()
|
static let shared = MacBundle()
|
||||||
|
|
||||||
private var bridge: MacBridge!
|
private var bridge: MacBridge!
|
||||||
|
|
||||||
private lazy var bridgeDelegate = MacBundleDelegate(bundle: self)
|
private lazy var bridgeDelegate = MacBundleDelegate(bundle: self)
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func configure() {
|
func configure() {
|
||||||
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent(Constants.Plugins.macBridgeName) else {
|
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent(Constants.Plugins.macBridgeName) else {
|
||||||
|
@ -46,11 +46,11 @@ class MacBundle {
|
||||||
bridge = bridgeClass.init()
|
bridge = bridgeClass.init()
|
||||||
bridge.menu.delegate = bridgeDelegate
|
bridge.menu.delegate = bridgeDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
var utils: MacUtils {
|
var utils: MacUtils {
|
||||||
bridge.utils
|
bridge.utils
|
||||||
}
|
}
|
||||||
|
|
||||||
var menu: MacMenu {
|
var menu: MacMenu {
|
||||||
bridge.menu
|
bridge.menu
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,17 +27,17 @@ import Foundation
|
||||||
|
|
||||||
class MacBundleDelegate: MacMenuDelegate {
|
class MacBundleDelegate: MacMenuDelegate {
|
||||||
private weak var bundle: MacBundle?
|
private weak var bundle: MacBundle?
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var profileManager: LightProfileManager {
|
var profileManager: LightProfileManager {
|
||||||
DefaultLightProfileManager()
|
DefaultLightProfileManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var providerManager: LightProviderManager {
|
var providerManager: LightProviderManager {
|
||||||
DefaultLightProviderManager()
|
DefaultLightProviderManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var vpnManager: LightVPNManager {
|
var vpnManager: LightVPNManager {
|
||||||
DefaultLightVPNManager()
|
DefaultLightVPNManager()
|
||||||
|
@ -46,7 +46,7 @@ class MacBundleDelegate: MacMenuDelegate {
|
||||||
var utils: LightUtils {
|
var utils: LightUtils {
|
||||||
DefaultLightUtils()
|
DefaultLightUtils()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(bundle: MacBundle?) {
|
init(bundle: MacBundle?) {
|
||||||
self.bundle = bundle
|
self.bundle = bundle
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,17 +29,17 @@ import Combine
|
||||||
|
|
||||||
class DefaultLightProfile: LightProfile {
|
class DefaultLightProfile: LightProfile {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
|
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
let vpnProtocol: String
|
let vpnProtocol: String
|
||||||
|
|
||||||
let isActive: Bool
|
let isActive: Bool
|
||||||
|
|
||||||
let providerName: String?
|
let providerName: String?
|
||||||
|
|
||||||
let providerServer: LightProviderServer?
|
let providerServer: LightProviderServer?
|
||||||
|
|
||||||
init(_ header: Profile.Header, vpnProtocol: String, isActive: Bool, providerServer: LightProviderServer?) {
|
init(_ header: Profile.Header, vpnProtocol: String, isActive: Bool, providerServer: LightProviderServer?) {
|
||||||
id = header.id
|
id = header.id
|
||||||
name = header.name
|
name = header.name
|
||||||
|
@ -52,11 +52,11 @@ class DefaultLightProfile: LightProfile {
|
||||||
|
|
||||||
class DefaultLightProfileManager: LightProfileManager {
|
class DefaultLightProfileManager: LightProfileManager {
|
||||||
private let profileManager = ProfileManager.shared
|
private let profileManager = ProfileManager.shared
|
||||||
|
|
||||||
private let providerManager = ProviderManager.shared
|
private let providerManager = ProviderManager.shared
|
||||||
|
|
||||||
private var subscriptions: Set<AnyCancellable> = []
|
private var subscriptions: Set<AnyCancellable> = []
|
||||||
|
|
||||||
weak var delegate: LightProfileManagerDelegate?
|
weak var delegate: LightProfileManagerDelegate?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -66,7 +66,7 @@ class DefaultLightProfileManager: LightProfileManager {
|
||||||
self.delegate?.didUpdateProfiles()
|
self.delegate?.didUpdateProfiles()
|
||||||
}.store(in: &subscriptions)
|
}.store(in: &subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasProfiles: Bool {
|
var hasProfiles: Bool {
|
||||||
profileManager.hasProfiles
|
profileManager.hasProfiles
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,9 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
class DefaultLightProviderCategory: LightProviderCategory {
|
class DefaultLightProviderCategory: LightProviderCategory {
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
var locations: [LightProviderLocation]
|
var locations: [LightProviderLocation]
|
||||||
|
|
||||||
init(_ category: ProviderCategory) {
|
init(_ category: ProviderCategory) {
|
||||||
name = category.name
|
name = category.name
|
||||||
locations = category.locations
|
locations = category.locations
|
||||||
|
@ -42,13 +42,13 @@ class DefaultLightProviderCategory: LightProviderCategory {
|
||||||
|
|
||||||
class DefaultLightProviderLocation: LightProviderLocation {
|
class DefaultLightProviderLocation: LightProviderLocation {
|
||||||
let description: String
|
let description: String
|
||||||
|
|
||||||
let id: String
|
let id: String
|
||||||
|
|
||||||
let countryCode: String
|
let countryCode: String
|
||||||
|
|
||||||
let servers: [LightProviderServer]
|
let servers: [LightProviderServer]
|
||||||
|
|
||||||
init(_ location: ProviderLocation) {
|
init(_ location: ProviderLocation) {
|
||||||
description = location.localizedCountry
|
description = location.localizedCountry
|
||||||
id = location.id
|
id = location.id
|
||||||
|
@ -61,15 +61,15 @@ class DefaultLightProviderLocation: LightProviderLocation {
|
||||||
|
|
||||||
class DefaultLightProviderServer: LightProviderServer {
|
class DefaultLightProviderServer: LightProviderServer {
|
||||||
let description: String
|
let description: String
|
||||||
|
|
||||||
let longDescription: String
|
let longDescription: String
|
||||||
|
|
||||||
let categoryName: String
|
let categoryName: String
|
||||||
|
|
||||||
let locationId: String
|
let locationId: String
|
||||||
|
|
||||||
let serverId: String
|
let serverId: String
|
||||||
|
|
||||||
init(_ server: ProviderServer) {
|
init(_ server: ProviderServer) {
|
||||||
description = server.localizedShortDescriptionWithDefault
|
description = server.localizedShortDescriptionWithDefault
|
||||||
longDescription = server.localizedLongDescription(withCategory: false)
|
longDescription = server.localizedLongDescription(withCategory: false)
|
||||||
|
@ -81,11 +81,11 @@ class DefaultLightProviderServer: LightProviderServer {
|
||||||
|
|
||||||
class DefaultLightProviderManager: LightProviderManager {
|
class DefaultLightProviderManager: LightProviderManager {
|
||||||
private let providerManager = ProviderManager.shared
|
private let providerManager = ProviderManager.shared
|
||||||
|
|
||||||
private var subscriptions: Set<AnyCancellable> = []
|
private var subscriptions: Set<AnyCancellable> = []
|
||||||
|
|
||||||
weak var delegate: LightProviderManagerDelegate?
|
weak var delegate: LightProviderManagerDelegate?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
providerManager.didUpdateProviders
|
providerManager.didUpdateProviders
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -93,7 +93,7 @@ class DefaultLightProviderManager: LightProviderManager {
|
||||||
self.delegate?.didUpdateProviders()
|
self.delegate?.didUpdateProviders()
|
||||||
}.store(in: &subscriptions)
|
}.store(in: &subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func categories(_ name: String, vpnProtocol: String) -> [LightProviderCategory] {
|
func categories(_ name: String, vpnProtocol: String) -> [LightProviderCategory] {
|
||||||
guard let vpnProtocolType = VPNProtocolType(rawValue: vpnProtocol) else {
|
guard let vpnProtocolType = VPNProtocolType(rawValue: vpnProtocol) else {
|
||||||
fatalError("Unrecognized VPN protocol: \(vpnProtocol)")
|
fatalError("Unrecognized VPN protocol: \(vpnProtocol)")
|
||||||
|
|
|
@ -28,18 +28,18 @@ import SwiftUI
|
||||||
|
|
||||||
class DefaultLightUtils: LightUtils {
|
class DefaultLightUtils: LightUtils {
|
||||||
private let app: UIApplication
|
private let app: UIApplication
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
app = .shared
|
app = .shared
|
||||||
}
|
}
|
||||||
|
|
||||||
@AppStorage(AppPreference.launchesOnLogin.key) var launchesOnLogin = false
|
@AppStorage(AppPreference.launchesOnLogin.key) var launchesOnLogin = false
|
||||||
|
|
||||||
func requestScene() {
|
func requestScene() {
|
||||||
guard app.connectedScenes.isEmpty else {
|
guard app.connectedScenes.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.requestSceneSessionActivation(nil, userActivity: nil, options: nil) { error in
|
app.requestSceneSessionActivation(nil, userActivity: nil, options: nil) { _ in
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,17 +29,17 @@ import Combine
|
||||||
|
|
||||||
class DefaultLightVPNManager: LightVPNManager {
|
class DefaultLightVPNManager: LightVPNManager {
|
||||||
private let vpnManager = VPNManager.shared
|
private let vpnManager = VPNManager.shared
|
||||||
|
|
||||||
private var subscriptions: Set<AnyCancellable> = []
|
private var subscriptions: Set<AnyCancellable> = []
|
||||||
|
|
||||||
var isEnabled: Bool {
|
var isEnabled: Bool {
|
||||||
vpnManager.currentState.isEnabled
|
vpnManager.currentState.isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
var vpnStatus: LightVPNStatus {
|
var vpnStatus: LightVPNStatus {
|
||||||
vpnManager.currentState.vpnStatus.asLightVPNStatus
|
vpnManager.currentState.vpnStatus.asLightVPNStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
private var delegates: [String: LightVPNManagerDelegate] = [:]
|
private var delegates: [String: LightVPNManagerDelegate] = [:]
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -63,7 +63,7 @@ class DefaultLightVPNManager: LightVPNManager {
|
||||||
)
|
)
|
||||||
}.store(in: &subscriptions)
|
}.store(in: &subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect(with profileId: UUID) {
|
func connect(with profileId: UUID) {
|
||||||
Task {
|
Task {
|
||||||
try? await vpnManager.connect(with: profileId)
|
try? await vpnManager.connect(with: profileId)
|
||||||
|
@ -75,7 +75,7 @@ class DefaultLightVPNManager: LightVPNManager {
|
||||||
try? await vpnManager.connect(with: profileId, toServer: serverId)
|
try? await vpnManager.connect(with: profileId, toServer: serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
Task {
|
Task {
|
||||||
await vpnManager.disable()
|
await vpnManager.disable()
|
||||||
|
@ -97,11 +97,11 @@ class DefaultLightVPNManager: LightVPNManager {
|
||||||
await vpnManager.reconnect()
|
await vpnManager.reconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addDelegate(_ delegate: LightVPNManagerDelegate, withIdentifier identifier: String) {
|
func addDelegate(_ delegate: LightVPNManagerDelegate, withIdentifier identifier: String) {
|
||||||
delegates[identifier] = delegate
|
delegates[identifier] = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeDelegate(withIdentifier identifier: String) {
|
func removeDelegate(withIdentifier identifier: String) {
|
||||||
delegates.removeValue(forKey: identifier)
|
delegates.removeValue(forKey: identifier)
|
||||||
}
|
}
|
||||||
|
@ -120,13 +120,13 @@ private extension VPNStatus {
|
||||||
switch self {
|
switch self {
|
||||||
case .connected:
|
case .connected:
|
||||||
return .connected
|
return .connected
|
||||||
|
|
||||||
case .connecting:
|
case .connecting:
|
||||||
return .connecting
|
return .connecting
|
||||||
|
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
return .disconnected
|
return .disconnected
|
||||||
|
|
||||||
case .disconnecting:
|
case .disconnecting:
|
||||||
return .disconnecting
|
return .disconnecting
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,9 @@ struct AddingTextField<Field: View, ActionLabel: View>: View {
|
||||||
let textField: (@escaping () -> Void) -> Field
|
let textField: (@escaping () -> Void) -> Field
|
||||||
|
|
||||||
let addLabel: () -> ActionLabel
|
let addLabel: () -> ActionLabel
|
||||||
|
|
||||||
var commitLabel: (() -> ActionLabel)?
|
var commitLabel: (() -> ActionLabel)?
|
||||||
|
|
||||||
@State private var isAdding = false
|
@State private var isAdding = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -51,14 +51,14 @@ struct AddingTextField<Field: View, ActionLabel: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func doAdd() {
|
private func doAdd() {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
onAdd?()
|
onAdd?()
|
||||||
isAdding = true
|
isAdding = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func doCommit() {
|
private func doCommit() {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
onCommit?()
|
onCommit?()
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
|
func ??<T>(lhs: Binding<T?>, rhs: T) -> Binding<T> {
|
||||||
Binding {
|
Binding {
|
||||||
lhs.wrappedValue ?? rhs
|
lhs.wrappedValue ?? rhs
|
||||||
} set: {
|
} set: {
|
||||||
|
@ -34,15 +34,15 @@ func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Binding {
|
extension Binding {
|
||||||
func toString() -> Binding<String> where Value == Optional<URL> {
|
func toString() -> Binding<String> where Value == URL? {
|
||||||
.init {
|
.init {
|
||||||
wrappedValue?.absoluteString ?? ""
|
wrappedValue?.absoluteString ?? ""
|
||||||
} set: {
|
} set: {
|
||||||
wrappedValue = URL(string: $0) ?? wrappedValue
|
wrappedValue = URL(string: $0) ?? wrappedValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toString<T>() -> Binding<String> where Value == Optional<T>, T: FixedWidthInteger {
|
func toString<T>() -> Binding<String> where Value == T?, T: FixedWidthInteger {
|
||||||
.init {
|
.init {
|
||||||
guard let v = wrappedValue else {
|
guard let v = wrappedValue else {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -29,17 +29,17 @@ struct CopySavingButton<T: Equatable, Label: View>: View {
|
||||||
@Binding var original: T
|
@Binding var original: T
|
||||||
|
|
||||||
@Binding var copy: T
|
@Binding var copy: T
|
||||||
|
|
||||||
var mapping: (T) -> T
|
var mapping: (T) -> T
|
||||||
|
|
||||||
let label: () -> Label
|
let label: () -> Label
|
||||||
|
|
||||||
var saveAnyway = false
|
var saveAnyway = false
|
||||||
|
|
||||||
var onSave: (() -> Void)?
|
var onSave: (() -> Void)?
|
||||||
|
|
||||||
@State private var isLoaded = false
|
@State private var isLoaded = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: saveToOriginal, label: label)
|
Button(action: saveToOriginal, label: label)
|
||||||
.disabled(!canSave)
|
.disabled(!canSave)
|
||||||
|
|
|
@ -27,9 +27,9 @@ import SwiftUI
|
||||||
|
|
||||||
struct DestructiveButton<Label: View>: View {
|
struct DestructiveButton<Label: View>: View {
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
let label: () -> Label
|
let label: () -> Label
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 15, *) {
|
if #available(iOS 15, *) {
|
||||||
Button(role: .destructive, action: action, label: label)
|
Button(role: .destructive, action: action, label: label)
|
||||||
|
|
|
@ -38,21 +38,21 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
onEditingChanged: (Bool) -> Void,
|
onEditingChanged: (Bool) -> Void,
|
||||||
onCommit: () -> Void
|
onCommit: () -> Void
|
||||||
)
|
)
|
||||||
|
|
||||||
@Binding var elements: [String]
|
@Binding var elements: [String]
|
||||||
|
|
||||||
var allowsDuplicates = true
|
var allowsDuplicates = true
|
||||||
|
|
||||||
var mapping: ([IdentifiableString]) -> [IdentifiableString] = { $0 }
|
var mapping: ([IdentifiableString]) -> [IdentifiableString] = { $0 }
|
||||||
|
|
||||||
var onAdd: ((Binding<String>) -> Void)?
|
var onAdd: ((Binding<String>) -> Void)?
|
||||||
|
|
||||||
let textField: (FieldCallback) -> Field
|
let textField: (FieldCallback) -> Field
|
||||||
|
|
||||||
let addLabel: () -> ActionLabel
|
let addLabel: () -> ActionLabel
|
||||||
|
|
||||||
var commitLabel: (() -> ActionLabel)?
|
var commitLabel: (() -> ActionLabel)?
|
||||||
|
|
||||||
@State private var isLoaded = false
|
@State private var isLoaded = false
|
||||||
|
|
||||||
@State private var identifiableElements: [IdentifiableString] = []
|
@State private var identifiableElements: [IdentifiableString] = []
|
||||||
|
@ -60,7 +60,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
@State private var editedTextStrings: [UUID: String] = [:]
|
@State private var editedTextStrings: [UUID: String] = [:]
|
||||||
|
|
||||||
private let addedUUID = UUID()
|
private let addedUUID = UUID()
|
||||||
|
|
||||||
private var addedText: Binding<String> {
|
private var addedText: Binding<String> {
|
||||||
.init {
|
.init {
|
||||||
editedTextStrings[addedUUID] ?? ""
|
editedTextStrings[addedUUID] ?? ""
|
||||||
|
@ -68,7 +68,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
editedTextStrings[addedUUID] = $0
|
editedTextStrings[addedUUID] = $0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Group {
|
return Group {
|
||||||
|
@ -87,7 +87,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
}
|
}
|
||||||
}.onChange(of: elements, perform: remapElements)
|
}.onChange(of: elements, perform: remapElements)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func existingRow(_ element: IdentifiableString) -> some View {
|
private func existingRow(_ element: IdentifiableString) -> some View {
|
||||||
let editedText = binding(toEditedElement: element)
|
let editedText = binding(toEditedElement: element)
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
|
||||||
replaceElement(at: element.id, with: editedText)
|
replaceElement(at: element.id, with: editedText)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var newRow: some View {
|
private var newRow: some View {
|
||||||
AddingTextField(
|
AddingTextField(
|
||||||
onAdd: {
|
onAdd: {
|
||||||
|
@ -144,7 +144,7 @@ extension EditableTextList {
|
||||||
identifiableElements = newIdentifiableElements
|
identifiableElements = newIdentifiableElements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addElement() {
|
private func addElement() {
|
||||||
guard allowsDuplicates || !identifiableElements.contains(where: {
|
guard allowsDuplicates || !identifiableElements.contains(where: {
|
||||||
$0.string == addedText.wrappedValue
|
$0.string == addedText.wrappedValue
|
||||||
|
@ -155,7 +155,7 @@ extension EditableTextList {
|
||||||
identifiableElements.append(.init(string: addedText.wrappedValue))
|
identifiableElements.append(.init(string: addedText.wrappedValue))
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func binding(toEditedElement element: IdentifiableString) -> Binding<String> {
|
private func binding(toEditedElement element: IdentifiableString) -> Binding<String> {
|
||||||
// print(">>> <-> \(element)")
|
// print(">>> <-> \(element)")
|
||||||
.init {
|
.init {
|
||||||
|
@ -184,14 +184,14 @@ extension EditableTextList {
|
||||||
}
|
}
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onDelete(offsets: IndexSet) {
|
private func onDelete(offsets: IndexSet) {
|
||||||
var mapped = mapping(identifiableElements)
|
var mapped = mapping(identifiableElements)
|
||||||
mapped.remove(atOffsets: offsets)
|
mapped.remove(atOffsets: offsets)
|
||||||
identifiableElements = mapped
|
identifiableElements = mapped
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onMove(indexSet: IndexSet, to: Int) {
|
private func onMove(indexSet: IndexSet, to: Int) {
|
||||||
var mapped = mapping(identifiableElements)
|
var mapped = mapping(identifiableElements)
|
||||||
mapped.move(fromOffsets: indexSet, toOffset: to)
|
mapped.move(fromOffsets: indexSet, toOffset: to)
|
||||||
|
|
|
@ -27,23 +27,23 @@ import SwiftUI
|
||||||
|
|
||||||
struct GenericCreditsView: View {
|
struct GenericCreditsView: View {
|
||||||
typealias License = (String, String, URL)
|
typealias License = (String, String, URL)
|
||||||
|
|
||||||
typealias Notice = (String, String)
|
typealias Notice = (String, String)
|
||||||
|
|
||||||
var licensesHeader: String? = "Licenses"
|
var licensesHeader: String? = "Licenses"
|
||||||
|
|
||||||
var noticesHeader: String? = "Notices"
|
var noticesHeader: String? = "Notices"
|
||||||
|
|
||||||
var translationsHeader: String? = "Translations"
|
var translationsHeader: String? = "Translations"
|
||||||
|
|
||||||
let licenses: [License]
|
let licenses: [License]
|
||||||
|
|
||||||
let notices: [Notice]
|
let notices: [Notice]
|
||||||
|
|
||||||
let translations: [String: String]
|
let translations: [String: String]
|
||||||
|
|
||||||
@State private var contentForLicense: [String: String] = [:]
|
@State private var contentForLicense: [String: String] = [:]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
if !licenses.isEmpty {
|
if !licenses.isEmpty {
|
||||||
|
@ -57,27 +57,27 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sortedLicenses: [License] {
|
private var sortedLicenses: [License] {
|
||||||
licenses.sorted {
|
licenses.sorted {
|
||||||
$0.0.lowercased() < $1.0.lowercased()
|
$0.0.lowercased() < $1.0.lowercased()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sortedNotices: [Notice] {
|
private var sortedNotices: [Notice] {
|
||||||
notices.sorted {
|
notices.sorted {
|
||||||
$0.0.lowercased() < $1.0.lowercased()
|
$0.0.lowercased() < $1.0.lowercased()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sortedLanguages: [String] {
|
private var sortedLanguages: [String] {
|
||||||
translations.keys.sorted {
|
translations.keys.sorted {
|
||||||
$0.localizedAsCountryCode < $1.localizedAsCountryCode
|
$0.localizedAsCountryCode < $1.localizedAsCountryCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var licensesSection: some View {
|
private var licensesSection: some View {
|
||||||
Section (
|
Section(
|
||||||
header: licensesHeader.map(Text.init)
|
header: licensesHeader.map(Text.init)
|
||||||
) {
|
) {
|
||||||
ForEach(sortedLicenses, id: \.0) { license in
|
ForEach(sortedLicenses, id: \.0) { license in
|
||||||
|
@ -98,7 +98,7 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var noticesSection: some View {
|
private var noticesSection: some View {
|
||||||
Section (
|
Section(
|
||||||
header: noticesHeader.map(Text.init)
|
header: noticesHeader.map(Text.init)
|
||||||
) {
|
) {
|
||||||
ForEach(sortedNotices, id: \.0) { notice in
|
ForEach(sortedNotices, id: \.0) { notice in
|
||||||
|
@ -108,7 +108,7 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var translationsSection: some View {
|
private var translationsSection: some View {
|
||||||
Section (
|
Section(
|
||||||
header: translationsHeader.map(Text.init)
|
header: translationsHeader.map(Text.init)
|
||||||
) {
|
) {
|
||||||
ForEach(sortedLanguages, id: \.self) { code in
|
ForEach(sortedLanguages, id: \.self) { code in
|
||||||
|
@ -123,7 +123,7 @@ struct GenericCreditsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func noticeView(_ content: (String, String)) -> some View {
|
private func noticeView(_ content: (String, String)) -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text(content.1)
|
Text(content.1)
|
||||||
|
@ -137,9 +137,9 @@ struct GenericCreditsView: View {
|
||||||
extension GenericCreditsView {
|
extension GenericCreditsView {
|
||||||
struct LicenseView: View {
|
struct LicenseView: View {
|
||||||
let url: URL
|
let url: URL
|
||||||
|
|
||||||
@Binding var content: String?
|
@Binding var content: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
content.map { unwrapped in
|
content.map { unwrapped in
|
||||||
|
@ -154,7 +154,7 @@ extension GenericCreditsView {
|
||||||
}
|
}
|
||||||
}.onAppear(perform: loadURL)
|
}.onAppear(perform: loadURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadURL() {
|
private func loadURL() {
|
||||||
guard content == nil else {
|
guard content == nil else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -27,13 +27,13 @@ import SwiftUI
|
||||||
|
|
||||||
struct GenericVersionView: View {
|
struct GenericVersionView: View {
|
||||||
let logoName: String
|
let logoName: String
|
||||||
|
|
||||||
let appName: String
|
let appName: String
|
||||||
|
|
||||||
let versionString: String
|
let versionString: String
|
||||||
|
|
||||||
let extraString: String?
|
let extraString: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Image(logoName)
|
Image(logoName)
|
||||||
|
|
|
@ -27,7 +27,7 @@ import SwiftUI
|
||||||
|
|
||||||
public struct IntentActivity<UserObject> {
|
public struct IntentActivity<UserObject> {
|
||||||
public let name: String
|
public let name: String
|
||||||
|
|
||||||
public let handler: (NSUserActivity, UserObject) -> Void
|
public let handler: (NSUserActivity, UserObject) -> Void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,9 @@ import IntentsUI
|
||||||
|
|
||||||
struct IntentAddView: UIViewControllerRepresentable {
|
struct IntentAddView: UIViewControllerRepresentable {
|
||||||
let shortcut: INShortcut
|
let shortcut: INShortcut
|
||||||
|
|
||||||
let delegate: INUIAddVoiceShortcutViewControllerDelegate?
|
let delegate: INUIAddVoiceShortcutViewControllerDelegate?
|
||||||
|
|
||||||
func makeUIViewController(context: UIViewControllerRepresentableContext<IntentAddView>) -> INUIAddVoiceShortcutViewController {
|
func makeUIViewController(context: UIViewControllerRepresentableContext<IntentAddView>) -> INUIAddVoiceShortcutViewController {
|
||||||
let vc = INUIAddVoiceShortcutViewController(shortcut: shortcut)
|
let vc = INUIAddVoiceShortcutViewController(shortcut: shortcut)
|
||||||
vc.delegate = delegate
|
vc.delegate = delegate
|
||||||
|
|
|
@ -29,9 +29,9 @@ import IntentsUI
|
||||||
|
|
||||||
struct IntentEditView: UIViewControllerRepresentable {
|
struct IntentEditView: UIViewControllerRepresentable {
|
||||||
let shortcut: Shortcut
|
let shortcut: Shortcut
|
||||||
|
|
||||||
let delegate: INUIEditVoiceShortcutViewControllerDelegate?
|
let delegate: INUIEditVoiceShortcutViewControllerDelegate?
|
||||||
|
|
||||||
func makeUIViewController(context: UIViewControllerRepresentableContext<IntentEditView>) -> INUIEditVoiceShortcutViewController {
|
func makeUIViewController(context: UIViewControllerRepresentableContext<IntentEditView>) -> INUIEditVoiceShortcutViewController {
|
||||||
let vc = INUIEditVoiceShortcutViewController(voiceShortcut: shortcut.native)
|
let vc = INUIEditVoiceShortcutViewController(voiceShortcut: shortcut.native)
|
||||||
vc.delegate = delegate
|
vc.delegate = delegate
|
||||||
|
|
|
@ -27,7 +27,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct LongContentView: View {
|
struct LongContentView: View {
|
||||||
@Binding var content: String
|
@Binding var content: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TextEditor(text: $content)
|
TextEditor(text: $content)
|
||||||
.font(.system(.body, design: .monospaced))
|
.font(.system(.body, design: .monospaced))
|
||||||
|
@ -39,13 +39,13 @@ struct LongContentView: View {
|
||||||
|
|
||||||
struct LongContentLink<Preview: View>: View {
|
struct LongContentLink<Preview: View>: View {
|
||||||
private let title: String
|
private let title: String
|
||||||
|
|
||||||
@Binding private var content: String
|
@Binding private var content: String
|
||||||
|
|
||||||
private let preview: String?
|
private let preview: String?
|
||||||
|
|
||||||
private let previewLabel: ((String) -> Preview)?
|
private let previewLabel: ((String) -> Preview)?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
_ title: String,
|
_ title: String,
|
||||||
content: Binding<String>,
|
content: Binding<String>,
|
||||||
|
@ -57,7 +57,7 @@ struct LongContentLink<Preview: View>: View {
|
||||||
self.preview = preview
|
self.preview = preview
|
||||||
self.previewLabel = previewLabel
|
self.previewLabel = previewLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
LongContentView(content: $content)
|
LongContentView(content: $content)
|
||||||
|
|
|
@ -29,32 +29,32 @@ import MessageUI
|
||||||
struct MailComposerView: UIViewControllerRepresentable {
|
struct MailComposerView: UIViewControllerRepresentable {
|
||||||
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
|
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
|
||||||
@Binding private var isPresented: Bool
|
@Binding private var isPresented: Bool
|
||||||
|
|
||||||
init(_ view: MailComposerView) {
|
init(_ view: MailComposerView) {
|
||||||
_isPresented = view._isPresented
|
_isPresented = view._isPresented
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Attachment = (data: Data, mimeType: String, fileName: String)
|
typealias Attachment = (data: Data, mimeType: String, fileName: String)
|
||||||
|
|
||||||
static func canSendMail() -> Bool {
|
static func canSendMail() -> Bool {
|
||||||
MFMailComposeViewController.canSendMail()
|
MFMailComposeViewController.canSendMail()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
|
|
||||||
let toRecipients: [String]
|
let toRecipients: [String]
|
||||||
|
|
||||||
let subject: String
|
let subject: String
|
||||||
|
|
||||||
let messageBody: String
|
let messageBody: String
|
||||||
|
|
||||||
var attachments: [Attachment]?
|
var attachments: [Attachment]?
|
||||||
|
|
||||||
func makeUIViewController(context: UIViewControllerRepresentableContext<MailComposerView>) -> MFMailComposeViewController {
|
func makeUIViewController(context: UIViewControllerRepresentableContext<MailComposerView>) -> MFMailComposeViewController {
|
||||||
let vc = MFMailComposeViewController()
|
let vc = MFMailComposeViewController()
|
||||||
vc.setToRecipients(toRecipients)
|
vc.setToRecipients(toRecipients)
|
||||||
|
@ -69,7 +69,7 @@ struct MailComposerView: UIViewControllerRepresentable {
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: UIViewControllerRepresentableContext<MailComposerView>) {
|
func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: UIViewControllerRepresentableContext<MailComposerView>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(self)
|
Coordinator(self)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,15 +27,15 @@ import SwiftUI
|
||||||
|
|
||||||
struct RevealingSecureField<ImageContent: View>: View {
|
struct RevealingSecureField<ImageContent: View>: View {
|
||||||
let title: String
|
let title: String
|
||||||
|
|
||||||
@Binding private var text: String
|
@Binding private var text: String
|
||||||
|
|
||||||
private let conceilImage: () -> ImageContent
|
private let conceilImage: () -> ImageContent
|
||||||
|
|
||||||
private let revealImage: () -> ImageContent
|
private let revealImage: () -> ImageContent
|
||||||
|
|
||||||
@State private var isRevealed = false
|
@State private var isRevealed = false
|
||||||
|
|
||||||
init(
|
init(
|
||||||
_ title: String,
|
_ title: String,
|
||||||
text: Binding<String>,
|
text: Binding<String>,
|
||||||
|
@ -47,7 +47,7 @@ struct RevealingSecureField<ImageContent: View>: View {
|
||||||
self.conceilImage = conceilImage
|
self.conceilImage = conceilImage
|
||||||
self.revealImage = revealImage
|
self.revealImage = revealImage
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
if isRevealed {
|
if isRevealed {
|
||||||
|
|
|
@ -29,14 +29,14 @@ import StoreKit
|
||||||
public class Reviewer: ObservableObject {
|
public class Reviewer: ObservableObject {
|
||||||
private struct Keys {
|
private struct Keys {
|
||||||
static let eventCount = "Reviewer.EventCount"
|
static let eventCount = "Reviewer.EventCount"
|
||||||
|
|
||||||
static let lastVersion = "Reviewer.LastVersion"
|
static let lastVersion = "Reviewer.LastVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
private let defaults: UserDefaults
|
private let defaults: UserDefaults
|
||||||
|
|
||||||
public var eventCountBeforeRating: Int = .max
|
public var eventCountBeforeRating: Int = .max
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
defaults = .standard
|
defaults = .standard
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ public class Reviewer: ObservableObject {
|
||||||
count += eventCount
|
count += eventCount
|
||||||
defaults.set(count, forKey: Keys.eventCount)
|
defaults.set(count, forKey: Keys.eventCount)
|
||||||
print("Reviewer: Event reported for version \(currentVersion) (count: \(count), prompt: \(eventCountBeforeRating))")
|
print("Reviewer: Event reported for version \(currentVersion) (count: \(count), prompt: \(eventCountBeforeRating))")
|
||||||
|
|
||||||
guard count >= eventCountBeforeRating else {
|
guard count >= eventCountBeforeRating else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -74,11 +74,11 @@ public class Reviewer: ObservableObject {
|
||||||
|
|
||||||
defaults.removeObject(forKey: Keys.eventCount)
|
defaults.removeObject(forKey: Keys.eventCount)
|
||||||
defaults.set(currentVersion, forKey: Keys.lastVersion)
|
defaults.set(currentVersion, forKey: Keys.lastVersion)
|
||||||
|
|
||||||
requestReview()
|
requestReview()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// may or may not appear
|
// may or may not appear
|
||||||
private func requestReview() {
|
private func requestReview() {
|
||||||
guard let scene = UIApplication.shared.windows.first(where: { $0.windowScene != nil })?.windowScene else {
|
guard let scene = UIApplication.shared.windows.first(where: { $0.windowScene != nil })?.windowScene else {
|
||||||
|
|
|
@ -28,11 +28,11 @@ import Intents
|
||||||
|
|
||||||
struct Shortcut: Identifiable, Hashable, Comparable {
|
struct Shortcut: Identifiable, Hashable, Comparable {
|
||||||
let native: INVoiceShortcut
|
let native: INVoiceShortcut
|
||||||
|
|
||||||
init(_ native: INVoiceShortcut) {
|
init(_ native: INVoiceShortcut) {
|
||||||
self.native = native
|
self.native = native
|
||||||
}
|
}
|
||||||
|
|
||||||
var id: UUID {
|
var id: UUID {
|
||||||
native.identifier
|
native.identifier
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ struct Shortcut: Identifiable, Hashable, Comparable {
|
||||||
static func ==(lhs: Self, rhs: Self) -> Bool {
|
static func ==(lhs: Self, rhs: Self) -> Bool {
|
||||||
lhs.phrase == rhs.phrase
|
lhs.phrase == rhs.phrase
|
||||||
}
|
}
|
||||||
|
|
||||||
static func <(lhs: Self, rhs: Self) -> Bool {
|
static func <(lhs: Self, rhs: Self) -> Bool {
|
||||||
lhs.phrase < rhs.phrase
|
lhs.phrase < rhs.phrase
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,15 +27,15 @@ import SwiftUI
|
||||||
|
|
||||||
struct StyledPicker<T: Hashable, Label: View, Style: ListStyle>: View {
|
struct StyledPicker<T: Hashable, Label: View, Style: ListStyle>: View {
|
||||||
let title: String
|
let title: String
|
||||||
|
|
||||||
@Binding var selection: T
|
@Binding var selection: T
|
||||||
|
|
||||||
let values: [T]
|
let values: [T]
|
||||||
|
|
||||||
let pickerLabel: (T) -> Label
|
let pickerLabel: (T) -> Label
|
||||||
|
|
||||||
let selectionLabel: (T) -> Label
|
let selectionLabel: (T) -> Label
|
||||||
|
|
||||||
let listStyle: () -> Style
|
let listStyle: () -> Style
|
||||||
|
|
||||||
@State private var isPresented = false
|
@State private var isPresented = false
|
||||||
|
@ -49,7 +49,7 @@ struct StyledPicker<T: Hashable, Label: View, Style: ListStyle>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pickerView() -> some View {
|
private func pickerView() -> some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
|
|
|
@ -28,13 +28,13 @@ import Foundation
|
||||||
struct Validators {
|
struct Validators {
|
||||||
enum ValidationError: Error {
|
enum ValidationError: Error {
|
||||||
case notSet
|
case notSet
|
||||||
|
|
||||||
case empty
|
case empty
|
||||||
|
|
||||||
case ipAddress
|
case ipAddress
|
||||||
|
|
||||||
case domainName
|
case domainName
|
||||||
|
|
||||||
case url
|
case url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ struct Validators {
|
||||||
throw ValidationError.domainName
|
throw ValidationError.domainName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func url(_ string: String) throws {
|
static func url(_ string: String) throws {
|
||||||
guard let _ = URL(string: string) else {
|
guard let _ = URL(string: string) else {
|
||||||
throw ValidationError.url
|
throw ValidationError.url
|
||||||
|
|
|
@ -31,7 +31,7 @@ struct AboutView: View {
|
||||||
private let versionString = Constants.Global.appVersionString
|
private let versionString = Constants.Global.appVersionString
|
||||||
|
|
||||||
private let redditURL = Constants.URLs.subreddit
|
private let redditURL = Constants.URLs.subreddit
|
||||||
|
|
||||||
private let shareMessage = L10n.Global.Messages.share
|
private let shareMessage = L10n.Global.Messages.share
|
||||||
|
|
||||||
private let readmeURL = Constants.URLs.readme
|
private let readmeURL = Constants.URLs.readme
|
||||||
|
@ -55,7 +55,7 @@ struct AboutView: View {
|
||||||
}.themeSecondaryView()
|
}.themeSecondaryView()
|
||||||
.navigationTitle(L10n.About.title)
|
.navigationTitle(L10n.About.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var infoSection: some View {
|
private var infoSection: some View {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
|
@ -69,7 +69,7 @@ struct AboutView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var supportSection: some View {
|
private var supportSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Button(L10n.About.Items.JoinCommunity.caption) {
|
Button(L10n.About.Items.JoinCommunity.caption) {
|
||||||
|
|
|
@ -28,19 +28,19 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct AccountView: View {
|
struct AccountView: View {
|
||||||
@ObservedObject private var providerManager: ProviderManager
|
@ObservedObject private var providerManager: ProviderManager
|
||||||
|
|
||||||
private let providerName: ProviderName?
|
private let providerName: ProviderName?
|
||||||
|
|
||||||
private let vpnProtocol: VPNProtocolType
|
private let vpnProtocol: VPNProtocolType
|
||||||
|
|
||||||
@Binding private var account: Profile.Account
|
@Binding private var account: Profile.Account
|
||||||
|
|
||||||
private let saveAnyway: Bool
|
private let saveAnyway: Bool
|
||||||
|
|
||||||
private let onSave: (() -> Void)?
|
private let onSave: (() -> Void)?
|
||||||
|
|
||||||
@State private var liveAccount = Profile.Account()
|
@State private var liveAccount = Profile.Account()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
providerName: ProviderName?,
|
providerName: ProviderName?,
|
||||||
vpnProtocol: VPNProtocolType,
|
vpnProtocol: VPNProtocolType,
|
||||||
|
@ -55,7 +55,7 @@ struct AccountView: View {
|
||||||
self.saveAnyway = saveAnyway
|
self.saveAnyway = saveAnyway
|
||||||
self.onSave = onSave
|
self.onSave = onSave
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
|
@ -126,7 +126,7 @@ struct AccountView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openGuidanceURL(_ url: URL) {
|
private func openGuidanceURL(_ url: URL) {
|
||||||
URL.openURL(url)
|
URL.openURL(url)
|
||||||
}
|
}
|
||||||
|
@ -155,10 +155,10 @@ private extension Profile.Account.AuthenticationMethod {
|
||||||
switch self {
|
switch self {
|
||||||
case .persistent:
|
case .persistent:
|
||||||
return L10n.Account.Items.AuthenticationMethod.persistent
|
return L10n.Account.Items.AuthenticationMethod.persistent
|
||||||
|
|
||||||
case .interactive:
|
case .interactive:
|
||||||
return L10n.Account.Items.AuthenticationMethod.interactive
|
return L10n.Account.Items.AuthenticationMethod.interactive
|
||||||
|
|
||||||
case .totp:
|
case .totp:
|
||||||
return Unlocalized.Other.totp
|
return Unlocalized.Other.totp
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,15 +31,15 @@ import TunnelKitWireGuard
|
||||||
extension AddHostView {
|
extension AddHostView {
|
||||||
struct NameView: View {
|
struct NameView: View {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
private let url: URL
|
private let url: URL
|
||||||
|
|
||||||
private let deletingURLOnSuccess: Bool
|
private let deletingURLOnSuccess: Bool
|
||||||
|
|
||||||
private let bindings: AddProfileView.Bindings
|
private let bindings: AddProfileView.Bindings
|
||||||
|
|
||||||
@State private var viewModel = ViewModel()
|
@State private var viewModel = ViewModel()
|
||||||
|
|
||||||
@State private var isEnteringCredentials = false
|
@State private var isEnteringCredentials = false
|
||||||
|
|
||||||
private var isComplete: Bool {
|
private var isComplete: Bool {
|
||||||
|
@ -107,7 +107,7 @@ extension AddHostView {
|
||||||
completeSection
|
completeSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var encryptionSection: some View {
|
private var encryptionSection: some View {
|
||||||
Section {
|
Section {
|
||||||
SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) {
|
SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) {
|
||||||
|
@ -132,7 +132,7 @@ extension AddHostView {
|
||||||
themeErrorMessage(viewModel.errorMessage)
|
themeErrorMessage(viewModel.errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hiddenAccountLink: some View {
|
private var hiddenAccountLink: some View {
|
||||||
NavigationLink("", isActive: $isEnteringCredentials) {
|
NavigationLink("", isActive: $isEnteringCredentials) {
|
||||||
AddProfileView.AccountWrapperView(
|
AddProfileView.AccountWrapperView(
|
||||||
|
@ -153,11 +153,11 @@ extension AddHostView {
|
||||||
private func requestResourcePermissions() {
|
private func requestResourcePermissions() {
|
||||||
_ = url.startAccessingSecurityScopedResource()
|
_ = url.startAccessingSecurityScopedResource()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dropResourcePermissions() {
|
private func dropResourcePermissions() {
|
||||||
url.stopAccessingSecurityScopedResource()
|
url.stopAccessingSecurityScopedResource()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func alertOverwriteExistingProfile() -> Alert {
|
private func alertOverwriteExistingProfile() -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text(L10n.AddProfile.Shared.title),
|
title: Text(L10n.AddProfile.Shared.title),
|
||||||
|
|
|
@ -31,19 +31,19 @@ import TunnelKitWireGuard
|
||||||
extension AddHostView {
|
extension AddHostView {
|
||||||
struct ViewModel: Equatable {
|
struct ViewModel: Equatable {
|
||||||
private var isNamePreset = false
|
private var isNamePreset = false
|
||||||
|
|
||||||
var profileName = ""
|
var profileName = ""
|
||||||
|
|
||||||
private(set) var requiresPassphrase = false
|
private(set) var requiresPassphrase = false
|
||||||
|
|
||||||
var encryptionPassphrase = ""
|
var encryptionPassphrase = ""
|
||||||
|
|
||||||
var processedProfile: Profile = .placeholder
|
var processedProfile: Profile = .placeholder
|
||||||
|
|
||||||
private(set) var errorMessage: String?
|
private(set) var errorMessage: String?
|
||||||
|
|
||||||
var isAskingOverwrite = false
|
var isAskingOverwrite = false
|
||||||
|
|
||||||
mutating func presetName(withURL url: URL) {
|
mutating func presetName(withURL url: URL) {
|
||||||
guard !isNamePreset else {
|
guard !isNamePreset else {
|
||||||
return
|
return
|
||||||
|
@ -96,7 +96,7 @@ extension AddHostView {
|
||||||
setMessage(forParsingError: error)
|
setMessage(forParsingError: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
mutating func addProcessedProfile(to profileManager: ProfileManager) -> Bool {
|
mutating func addProcessedProfile(to profileManager: ProfileManager) -> Bool {
|
||||||
guard !processedProfile.isPlaceholder else {
|
guard !processedProfile.isPlaceholder else {
|
||||||
|
@ -107,7 +107,7 @@ extension AddHostView {
|
||||||
profileManager.saveProfile(processedProfile, isActive: nil)
|
profileManager.saveProfile(processedProfile, isActive: nil)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func setMessage(forParsingError error: Error) {
|
private mutating func setMessage(forParsingError error: Error) {
|
||||||
errorMessage = error.localizedVPNParsingDescription
|
errorMessage = error.localizedVPNParsingDescription
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ struct AddProfileMenu: View {
|
||||||
case addProvider
|
case addProvider
|
||||||
|
|
||||||
case addHost(URL, Bool)
|
case addHost(URL, Bool)
|
||||||
|
|
||||||
// XXX: alert ids
|
// XXX: alert ids
|
||||||
var id: Int {
|
var id: Int {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -41,16 +41,16 @@ struct AddProfileMenu: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
@Binding private var isHostFileImporterPresented: Bool
|
@Binding private var isHostFileImporterPresented: Bool
|
||||||
|
|
||||||
init(modalType: Binding<ModalType?>, isHostFileImporterPresented: Binding<Bool>) {
|
init(modalType: Binding<ModalType?>, isHostFileImporterPresented: Binding<Bool>) {
|
||||||
_modalType = modalType
|
_modalType = modalType
|
||||||
_isHostFileImporterPresented = isHostFileImporterPresented
|
_isHostFileImporterPresented = isHostFileImporterPresented
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Menu {
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
|
@ -74,7 +74,7 @@ struct AddProfileMenu: View {
|
||||||
themeAddMenuImage.asSystemImage
|
themeAddMenuImage.asSystemImage
|
||||||
}.sheet(item: $modalType, content: presentedModal)
|
}.sheet(item: $modalType, content: presentedModal)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func presentedModal(_ modalType: ModalType) -> some View {
|
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||||
switch modalType {
|
switch modalType {
|
||||||
|
@ -99,7 +99,7 @@ struct AddProfileMenu: View {
|
||||||
}.themeGlobal()
|
}.themeGlobal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isModalPresented: Binding<Bool> {
|
private var isModalPresented: Binding<Bool> {
|
||||||
.init {
|
.init {
|
||||||
modalType != nil
|
modalType != nil
|
||||||
|
|
|
@ -30,14 +30,14 @@ enum AddProfileView {
|
||||||
struct Bindings {
|
struct Bindings {
|
||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProfileNameSection: View {
|
struct ProfileNameSection: View {
|
||||||
@Binding var profileName: String
|
@Binding var profileName: String
|
||||||
|
|
||||||
let errorMessage: String?
|
let errorMessage: String?
|
||||||
|
|
||||||
let onCommit: () -> Void
|
let onCommit: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.Global.Placeholders.profileName, text: $profileName, onCommit: onCommit)
|
TextField(L10n.Global.Placeholders.profileName, text: $profileName, onCommit: onCommit)
|
||||||
|
@ -49,12 +49,12 @@ enum AddProfileView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExistingProfilesSection: View {
|
struct ExistingProfilesSection: View {
|
||||||
let headers: [Profile.Header]
|
let headers: [Profile.Header]
|
||||||
|
|
||||||
@Binding var profileName: String
|
@Binding var profileName: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
ForEach(headers, content: existingProfileButton)
|
ForEach(headers, content: existingProfileButton)
|
||||||
|
@ -62,7 +62,7 @@ enum AddProfileView {
|
||||||
Text(L10n.AddProfile.Shared.Views.Existing.header)
|
Text(L10n.AddProfile.Shared.Views.Existing.header)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func existingProfileButton(_ header: Profile.Header) -> some View {
|
private func existingProfileButton(_ header: Profile.Header) -> some View {
|
||||||
Button(header.name) {
|
Button(header.name) {
|
||||||
profileName = header.name
|
profileName = header.name
|
||||||
|
@ -74,11 +74,11 @@ enum AddProfileView {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
@Binding private var profile: Profile
|
@Binding private var profile: Profile
|
||||||
|
|
||||||
private let bindings: AddProfileView.Bindings
|
private let bindings: AddProfileView.Bindings
|
||||||
|
|
||||||
@State private var account = Profile.Account()
|
@State private var account = Profile.Account()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
profile: Binding<Profile>,
|
profile: Binding<Profile>,
|
||||||
bindings: AddProfileView.Bindings
|
bindings: AddProfileView.Bindings
|
||||||
|
@ -87,7 +87,7 @@ enum AddProfileView {
|
||||||
_profile = profile
|
_profile = profile
|
||||||
self.bindings = bindings
|
self.bindings = bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
AccountView(
|
AccountView(
|
||||||
providerName: profile.header.providerName,
|
providerName: profile.header.providerName,
|
||||||
|
|
|
@ -29,17 +29,17 @@ import PassepartoutLibrary
|
||||||
extension AddProviderView {
|
extension AddProviderView {
|
||||||
struct NameView: View {
|
struct NameView: View {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
@Binding private var profile: Profile
|
@Binding private var profile: Profile
|
||||||
|
|
||||||
private let providerMetadata: ProviderMetadata
|
private let providerMetadata: ProviderMetadata
|
||||||
|
|
||||||
private let bindings: AddProfileView.Bindings
|
private let bindings: AddProfileView.Bindings
|
||||||
|
|
||||||
@State private var viewModel = ViewModel()
|
@State private var viewModel = ViewModel()
|
||||||
|
|
||||||
@State private var isEnteringCredentials = false
|
@State private var isEnteringCredentials = false
|
||||||
|
|
||||||
init(
|
init(
|
||||||
profile: Binding<Profile>,
|
profile: Binding<Profile>,
|
||||||
providerMetadata: ProviderMetadata,
|
providerMetadata: ProviderMetadata,
|
||||||
|
@ -80,7 +80,7 @@ extension AddProviderView {
|
||||||
}.alert(isPresented: $viewModel.isAskingOverwrite, content: alertOverwriteExistingProfile)
|
}.alert(isPresented: $viewModel.isAskingOverwrite, content: alertOverwriteExistingProfile)
|
||||||
.navigationTitle(providerMetadata.fullName)
|
.navigationTitle(providerMetadata.fullName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hiddenAccountLink: some View {
|
private var hiddenAccountLink: some View {
|
||||||
NavigationLink("", isActive: $isEnteringCredentials) {
|
NavigationLink("", isActive: $isEnteringCredentials) {
|
||||||
AddProfileView.AccountWrapperView(
|
AddProfileView.AccountWrapperView(
|
||||||
|
|
|
@ -28,26 +28,26 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct AddProviderView: View {
|
struct AddProviderView: View {
|
||||||
@ObservedObject private var providerManager: ProviderManager
|
@ObservedObject private var providerManager: ProviderManager
|
||||||
|
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
private let bindings: AddProfileView.Bindings
|
private let bindings: AddProfileView.Bindings
|
||||||
|
|
||||||
@StateObject private var viewModel = ViewModel()
|
@StateObject private var viewModel = ViewModel()
|
||||||
|
|
||||||
init(bindings: AddProfileView.Bindings) {
|
init(bindings: AddProfileView.Bindings) {
|
||||||
providerManager = .shared
|
providerManager = .shared
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
self.bindings = bindings
|
self.bindings = bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
private var providers: [ProviderMetadata] {
|
private var providers: [ProviderMetadata] {
|
||||||
providerManager.allProviders()
|
providerManager.allProviders()
|
||||||
.filter {
|
.filter {
|
||||||
$0.supportedVPNProtocols.contains(viewModel.selectedVPNProtocol)
|
$0.supportedVPNProtocols.contains(viewModel.selectedVPNProtocol)
|
||||||
}.sorted()
|
}.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var availableVPNProtocols: [VPNProtocolType] {
|
private var availableVPNProtocols: [VPNProtocolType] {
|
||||||
var protos: Set<VPNProtocolType> = []
|
var protos: Set<VPNProtocolType> = []
|
||||||
providers.forEach {
|
providers.forEach {
|
||||||
|
@ -57,7 +57,7 @@ struct AddProviderView: View {
|
||||||
}
|
}
|
||||||
return protos.sorted()
|
return protos.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
ForEach(providers, id: \.navigationId, content: hiddenProviderLink)
|
ForEach(providers, id: \.navigationId, content: hiddenProviderLink)
|
||||||
|
@ -82,7 +82,7 @@ struct AddProviderView: View {
|
||||||
}.navigationTitle(L10n.AddProfile.Shared.title)
|
}.navigationTitle(L10n.AddProfile.Shared.title)
|
||||||
.themeSecondaryView()
|
.themeSecondaryView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainSection: some View {
|
private var mainSection: some View {
|
||||||
Section {
|
Section {
|
||||||
let protos = availableVPNProtocols
|
let protos = availableVPNProtocols
|
||||||
|
@ -99,7 +99,7 @@ struct AddProviderView: View {
|
||||||
Text(L10n.AddProfile.Provider.Sections.Vpn.footer)
|
Text(L10n.AddProfile.Provider.Sections.Vpn.footer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var providersSection: some View {
|
private var providersSection: some View {
|
||||||
Section {
|
Section {
|
||||||
ForEach(providers, content: providerRow)
|
ForEach(providers, content: providerRow)
|
||||||
|
@ -107,7 +107,7 @@ struct AddProviderView: View {
|
||||||
themeErrorMessage(viewModel.errorMessage)
|
themeErrorMessage(viewModel.errorMessage)
|
||||||
}.disabled(viewModel.isFetchingAnyProvider)
|
}.disabled(viewModel.isFetchingAnyProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func providerRow(_ metadata: ProviderMetadata) -> some View {
|
private func providerRow(_ metadata: ProviderMetadata) -> some View {
|
||||||
Button {
|
Button {
|
||||||
presentOrPurchaseProvider(metadata)
|
presentOrPurchaseProvider(metadata)
|
||||||
|
@ -115,7 +115,7 @@ struct AddProviderView: View {
|
||||||
Label(metadata.fullName, image: themeAssetsProviderImage(metadata.name))
|
Label(metadata.fullName, image: themeAssetsProviderImage(metadata.name))
|
||||||
}.withTrailingProgress(when: viewModel.isFetchingProvider(metadata.name))
|
}.withTrailingProgress(when: viewModel.isFetchingProvider(metadata.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hiddenProviderLink(_ metadata: ProviderMetadata) -> some View {
|
private func hiddenProviderLink(_ metadata: ProviderMetadata) -> some View {
|
||||||
NavigationLink("", tag: metadata, selection: $viewModel.selectedProvider) {
|
NavigationLink("", tag: metadata, selection: $viewModel.selectedProvider) {
|
||||||
NameView(
|
NameView(
|
||||||
|
@ -132,7 +132,7 @@ struct AddProviderView: View {
|
||||||
}.withTrailingProgress(when: viewModel.isUpdatingIndex)
|
}.withTrailingProgress(when: viewModel.isUpdatingIndex)
|
||||||
.disabled(viewModel.isUpdatingIndex)
|
.disabled(viewModel.isUpdatingIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eligibility: select or purchase provider
|
// eligibility: select or purchase provider
|
||||||
private func presentOrPurchaseProvider(_ metadata: ProviderMetadata) {
|
private func presentOrPurchaseProvider(_ metadata: ProviderMetadata) {
|
||||||
guard productManager.isEligible(forProvider: metadata.name) else {
|
guard productManager.isEligible(forProvider: metadata.name) else {
|
||||||
|
|
|
@ -30,7 +30,7 @@ extension AddProviderView {
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
enum PendingOperation {
|
enum PendingOperation {
|
||||||
case index
|
case index
|
||||||
|
|
||||||
case provider(ProviderName)
|
case provider(ProviderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,19 +54,19 @@ extension AddProviderView {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var selectedVPNProtocol: VPNProtocolType = .openVPN
|
@Published var selectedVPNProtocol: VPNProtocolType = .openVPN
|
||||||
|
|
||||||
@Published var selectedProvider: ProviderMetadata?
|
@Published var selectedProvider: ProviderMetadata?
|
||||||
|
|
||||||
@Published var pendingProfile: Profile = .placeholder
|
@Published var pendingProfile: Profile = .placeholder
|
||||||
|
|
||||||
@Published private(set) var pendingOperation: PendingOperation?
|
@Published private(set) var pendingOperation: PendingOperation?
|
||||||
|
|
||||||
@Published var isPaywallPresented = false
|
@Published var isPaywallPresented = false
|
||||||
|
|
||||||
@Published private(set) var errorMessage: String?
|
@Published private(set) var errorMessage: String?
|
||||||
|
|
||||||
func selectProvider(_ metadata: ProviderMetadata, _ providerManager: ProviderManager) {
|
func selectProvider(_ metadata: ProviderMetadata, _ providerManager: ProviderManager) {
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
guard let server = providerManager.anyDefaultServer(
|
guard let server = providerManager.anyDefaultServer(
|
||||||
|
@ -109,7 +109,7 @@ extension AddProviderView {
|
||||||
pendingProfile = Profile(metadata, server: server)
|
pendingProfile = Profile(metadata, server: server)
|
||||||
selectedProvider = metadata
|
selectedProvider = metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateIndex(_ providerManager: ProviderManager) {
|
func updateIndex(_ providerManager: ProviderManager) {
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
pendingOperation = .index
|
pendingOperation = .index
|
||||||
|
@ -136,11 +136,11 @@ extension AddProviderView.NameView {
|
||||||
private var isNamePreset = false
|
private var isNamePreset = false
|
||||||
|
|
||||||
var profileName = ""
|
var profileName = ""
|
||||||
|
|
||||||
var isAskingOverwrite = false
|
var isAskingOverwrite = false
|
||||||
|
|
||||||
private(set) var errorMessage: String?
|
private(set) var errorMessage: String?
|
||||||
|
|
||||||
mutating func presetName(withMetadata metadata: ProviderMetadata) {
|
mutating func presetName(withMetadata metadata: ProviderMetadata) {
|
||||||
guard !isNamePreset else {
|
guard !isNamePreset else {
|
||||||
return
|
return
|
||||||
|
@ -173,7 +173,7 @@ extension AddProviderView.NameView {
|
||||||
profileManager.saveProfile(finalProfile, isActive: nil)
|
profileManager.saveProfile(finalProfile, isActive: nil)
|
||||||
return finalProfile
|
return finalProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func setMessage(forError error: Error) {
|
private mutating func setMessage(forError error: Error) {
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,8 @@ import SwiftUI
|
||||||
struct CreditsView: View {
|
struct CreditsView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GenericCreditsView(
|
GenericCreditsView(
|
||||||
licensesHeader: nil,//L10n.Credits.Sections.Licenses.header,
|
licensesHeader: nil,// L10n.Credits.Sections.Licenses.header,
|
||||||
noticesHeader: nil,//L10n.Credits.Sections.Notices.header,
|
noticesHeader: nil,// L10n.Credits.Sections.Notices.header,
|
||||||
translationsHeader: L10n.Global.Strings.translations,
|
translationsHeader: L10n.Global.Strings.translations,
|
||||||
licenses: Unlocalized.Credits.licenses,
|
licenses: Unlocalized.Credits.licenses,
|
||||||
notices: Unlocalized.Credits.notices,
|
notices: Unlocalized.Credits.notices,
|
||||||
|
|
|
@ -29,23 +29,23 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct DebugLogView: View {
|
struct DebugLogView: View {
|
||||||
private let title: String
|
private let title: String
|
||||||
|
|
||||||
private let url: URL
|
private let url: URL
|
||||||
|
|
||||||
private let timer: AnyPublisher<Date, Never>
|
private let timer: AnyPublisher<Date, Never>
|
||||||
|
|
||||||
@State private var logLines: [String] = []
|
@State private var logLines: [String] = []
|
||||||
|
|
||||||
@State private var isSharing = false
|
@State private var isSharing = false
|
||||||
|
|
||||||
private let maxBytes = UInt64(Constants.Log.maxBytes)
|
private let maxBytes = UInt64(Constants.Log.maxBytes)
|
||||||
|
|
||||||
private let appName = Constants.Global.appName
|
private let appName = Constants.Global.appName
|
||||||
|
|
||||||
private let appVersion = Constants.Global.appVersionString
|
private let appVersion = Constants.Global.appVersionString
|
||||||
|
|
||||||
private let shareFilename = Unlocalized.Issues.Filenames.debugLog
|
private let shareFilename = Unlocalized.Issues.Filenames.debugLog
|
||||||
|
|
||||||
init(title: String, url: URL, refreshInterval: TimeInterval?) {
|
init(title: String, url: URL, refreshInterval: TimeInterval?) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.url = url
|
self.url = url
|
||||||
|
@ -58,7 +58,7 @@ struct DebugLogView: View {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollViewReader { scrollProxy in
|
ScrollViewReader { scrollProxy in
|
||||||
ScrollView(showsIndicators: true) {
|
ScrollView(showsIndicators: true) {
|
||||||
|
@ -96,22 +96,22 @@ struct DebugLogView: View {
|
||||||
Text(logLines[$0])
|
Text(logLines[$0])
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
}//.padding()
|
}// .padding()
|
||||||
// TODO: layout, a slight padding would be nice, but it glitches on first touch
|
// TODO: layout, a slight padding would be nice, but it glitches on first touch
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshLog(scrollingToLatestWith scrollProxy: ScrollViewProxy?) {
|
private func refreshLog(scrollingToLatestWith scrollProxy: ScrollViewProxy?) {
|
||||||
logLines = url.trailingLines(bytes: maxBytes)
|
logLines = url.trailingLines(bytes: maxBytes)
|
||||||
if let scrollProxy = scrollProxy {
|
if let scrollProxy = scrollProxy {
|
||||||
scrollToLatestUpdate(scrollProxy)
|
scrollToLatestUpdate(scrollProxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshLog(_: Date) {
|
private func refreshLog(_: Date) {
|
||||||
refreshLog(scrollingToLatestWith: nil)
|
refreshLog(scrollingToLatestWith: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DebugLogView {
|
extension DebugLogView {
|
||||||
private func shareDebugLog() {
|
private func shareDebugLog() {
|
||||||
guard !logLines.isEmpty else {
|
guard !logLines.isEmpty else {
|
||||||
|
@ -120,7 +120,7 @@ extension DebugLogView {
|
||||||
}
|
}
|
||||||
isSharing = true
|
isSharing = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sharingActivityView() -> some View {
|
private func sharingActivityView() -> some View {
|
||||||
ActivityView(activityItems: sharingItems)
|
ActivityView(activityItems: sharingItems)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ extension DiagnosticsView {
|
||||||
struct OpenVPNView: View {
|
struct OpenVPNView: View {
|
||||||
enum AlertType: Int, Identifiable {
|
enum AlertType: Int, Identifiable {
|
||||||
case emailNotConfigured
|
case emailNotConfigured
|
||||||
|
|
||||||
var id: Int {
|
var id: Int {
|
||||||
return rawValue
|
return rawValue
|
||||||
}
|
}
|
||||||
|
@ -40,23 +40,23 @@ extension DiagnosticsView {
|
||||||
@ObservedObject private var providerManager: ProviderManager
|
@ObservedObject private var providerManager: ProviderManager
|
||||||
|
|
||||||
@ObservedObject private var vpnManager: VPNManager
|
@ObservedObject private var vpnManager: VPNManager
|
||||||
|
|
||||||
@ObservedObject private var currentVPNState: ObservableVPNState
|
@ObservedObject private var currentVPNState: ObservableVPNState
|
||||||
|
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
private let providerName: ProviderName?
|
private let providerName: ProviderName?
|
||||||
|
|
||||||
private var isEligibleForFeedback: Bool {
|
private var isEligibleForFeedback: Bool {
|
||||||
productManager.isEligibleForFeedback()
|
productManager.isEligibleForFeedback()
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var isReportingIssue = false
|
@State private var isReportingIssue = false
|
||||||
|
|
||||||
@State private var alertType: AlertType?
|
@State private var alertType: AlertType?
|
||||||
|
|
||||||
private let vpnProtocol: VPNProtocolType = .openVPN
|
private let vpnProtocol: VPNProtocolType = .openVPN
|
||||||
|
|
||||||
init(providerName: ProviderName?) {
|
init(providerName: ProviderName?) {
|
||||||
providerManager = .shared
|
providerManager = .shared
|
||||||
vpnManager = .shared
|
vpnManager = .shared
|
||||||
|
@ -103,7 +103,7 @@ extension DiagnosticsView {
|
||||||
}.disabled(cfg == nil)
|
}.disabled(cfg == nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var debugLogSection: some View {
|
private var debugLogSection: some View {
|
||||||
Section {
|
Section {
|
||||||
DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL)
|
DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL)
|
||||||
|
@ -114,7 +114,7 @@ extension DiagnosticsView {
|
||||||
Text(L10n.Diagnostics.Sections.DebugLog.footer)
|
Text(L10n.Diagnostics.Sections.DebugLog.footer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var issueReporterSection: some View {
|
private var issueReporterSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Button(L10n.Diagnostics.Items.ReportIssue.caption, action: presentReportIssue)
|
Button(L10n.Diagnostics.Items.ReportIssue.caption, action: presentReportIssue)
|
||||||
|
|
|
@ -30,9 +30,9 @@ import TunnelKitWireGuard
|
||||||
extension DiagnosticsView {
|
extension DiagnosticsView {
|
||||||
struct WireGuardView: View {
|
struct WireGuardView: View {
|
||||||
@ObservedObject private var vpnManager: VPNManager
|
@ObservedObject private var vpnManager: VPNManager
|
||||||
|
|
||||||
private let providerName: ProviderName?
|
private let providerName: ProviderName?
|
||||||
|
|
||||||
init(providerName: ProviderName?) {
|
init(providerName: ProviderName?) {
|
||||||
vpnManager = .shared
|
vpnManager = .shared
|
||||||
self.providerName = providerName
|
self.providerName = providerName
|
||||||
|
|
|
@ -28,7 +28,7 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct DiagnosticsView: View {
|
struct DiagnosticsView: View {
|
||||||
let vpnProtocol: VPNProtocolType
|
let vpnProtocol: VPNProtocolType
|
||||||
|
|
||||||
let providerName: ProviderName?
|
let providerName: ProviderName?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -38,7 +38,7 @@ struct DiagnosticsView: View {
|
||||||
DiagnosticsView.OpenVPNView(
|
DiagnosticsView.OpenVPNView(
|
||||||
providerName: providerName
|
providerName: providerName
|
||||||
)
|
)
|
||||||
|
|
||||||
case .wireGuard:
|
case .wireGuard:
|
||||||
DiagnosticsView.WireGuardView(
|
DiagnosticsView.WireGuardView(
|
||||||
providerName: providerName
|
providerName: providerName
|
||||||
|
@ -51,9 +51,9 @@ struct DiagnosticsView: View {
|
||||||
extension DiagnosticsView {
|
extension DiagnosticsView {
|
||||||
struct DebugLogSection: View {
|
struct DebugLogSection: View {
|
||||||
let appLogURL: URL?
|
let appLogURL: URL?
|
||||||
|
|
||||||
let tunnelLogURL: URL?
|
let tunnelLogURL: URL?
|
||||||
|
|
||||||
private let refreshInterval = Constants.Log.refreshInterval
|
private let refreshInterval = Constants.Log.refreshInterval
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -68,7 +68,7 @@ extension DiagnosticsView {
|
||||||
refreshInterval: nil
|
refreshInterval: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tunnelLink: some View {
|
private var tunnelLink: some View {
|
||||||
navigationLink(
|
navigationLink(
|
||||||
withTitle: Unlocalized.VPN.vpn,
|
withTitle: Unlocalized.VPN.vpn,
|
||||||
|
|
|
@ -30,9 +30,9 @@ import PassepartoutLibrary
|
||||||
struct DonateView: View {
|
struct DonateView: View {
|
||||||
enum AlertType: Identifiable {
|
enum AlertType: Identifiable {
|
||||||
case thankYou
|
case thankYou
|
||||||
|
|
||||||
case purchaseFailed(Error)
|
case purchaseFailed(Error)
|
||||||
|
|
||||||
// XXX: alert ids
|
// XXX: alert ids
|
||||||
var id: Int {
|
var id: Int {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -42,19 +42,19 @@ struct DonateView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
@State private var alertType: AlertType?
|
@State private var alertType: AlertType?
|
||||||
|
|
||||||
@State private var pendingDonationIdentifier: String?
|
@State private var pendingDonationIdentifier: String?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
productsSection
|
productsSection
|
||||||
|
@ -90,7 +90,7 @@ struct DonateView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var productsSection: some View {
|
private var productsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
if !productManager.isRefreshingProducts {
|
if !productManager.isRefreshingProducts {
|
||||||
|
@ -104,7 +104,7 @@ struct DonateView: View {
|
||||||
Text(L10n.Donate.Sections.OneTime.footer)
|
Text(L10n.Donate.Sections.OneTime.footer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func productRow(_ product: SKProduct) -> some View {
|
private func productRow(_ product: SKProduct) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -129,7 +129,7 @@ extension DonateView {
|
||||||
pendingDonationIdentifier = product.productIdentifier
|
pendingDonationIdentifier = product.productIdentifier
|
||||||
productManager.purchase(product, completionHandler: handlePurchaseResult)
|
productManager.purchase(product, completionHandler: handlePurchaseResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handlePurchaseResult(_ result: Result<InAppPurchaseResult, Error>) {
|
private func handlePurchaseResult(_ result: Result<InAppPurchaseResult, Error>) {
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let value):
|
case .success(let value):
|
||||||
|
@ -138,7 +138,7 @@ extension DonateView {
|
||||||
} else {
|
} else {
|
||||||
// cancelled
|
// cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
alertType = .purchaseFailed(error)
|
alertType = .purchaseFailed(error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,11 +32,11 @@ extension EndpointAdvancedView {
|
||||||
@Binding var builder: OpenVPN.ConfigurationBuilder
|
@Binding var builder: OpenVPN.ConfigurationBuilder
|
||||||
|
|
||||||
let isReadonly: Bool
|
let isReadonly: Bool
|
||||||
|
|
||||||
let isServerPushed: Bool
|
let isServerPushed: Bool
|
||||||
|
|
||||||
private let fallbackConfiguration = OpenVPN.ConfigurationBuilder(withFallbacks: true).build()
|
private let fallbackConfiguration = OpenVPN.ConfigurationBuilder(withFallbacks: true).build()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
let cfg = builder.build()
|
let cfg = builder.build()
|
||||||
|
@ -104,7 +104,7 @@ extension EndpointAdvancedView.OpenVPNView {
|
||||||
Text(Unlocalized.Network.ipv4)
|
Text(Unlocalized.Network.ipv4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var ipv6Section: some View {
|
private var ipv6Section: some View {
|
||||||
Section {
|
Section {
|
||||||
if let settings = builder.ipv6 {
|
if let settings = builder.ipv6 {
|
||||||
|
@ -156,7 +156,7 @@ extension EndpointAdvancedView.OpenVPNView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var communicationEditableSection: some View {
|
private var communicationEditableSection: some View {
|
||||||
Section {
|
Section {
|
||||||
themeTextPicker(
|
themeTextPicker(
|
||||||
|
@ -185,7 +185,7 @@ extension EndpointAdvancedView.OpenVPNView {
|
||||||
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Communication.header)
|
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Communication.header)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func compressionSection(configuration: OpenVPN.Configuration) -> some View {
|
private func compressionSection(configuration: OpenVPN.Configuration) -> some View {
|
||||||
configuration.compressionSettings.map { settings in
|
configuration.compressionSettings.map { settings in
|
||||||
Section {
|
Section {
|
||||||
|
@ -202,7 +202,7 @@ extension EndpointAdvancedView.OpenVPNView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var compressionEditableSection: some View {
|
private var compressionEditableSection: some View {
|
||||||
Section {
|
Section {
|
||||||
themeTextPicker(
|
themeTextPicker(
|
||||||
|
@ -334,21 +334,21 @@ extension OpenVPN.Configuration {
|
||||||
}
|
}
|
||||||
return (compressionFraming, compressionAlgorithm)
|
return (compressionFraming, compressionAlgorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
var dnsSettings: (servers: [String], domains: [String])? {
|
var dnsSettings: (servers: [String], domains: [String])? {
|
||||||
guard !(dnsServers?.isEmpty ?? true) || !(searchDomains?.isEmpty ?? true) else {
|
guard !(dnsServers?.isEmpty ?? true) || !(searchDomains?.isEmpty ?? true) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return (dnsServers ?? [], searchDomains ?? [])
|
return (dnsServers ?? [], searchDomains ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxySettings: (proxy: Proxy?, pac: URL?, bypass: [String])? {
|
var proxySettings: (proxy: Proxy?, pac: URL?, bypass: [String])? {
|
||||||
guard httpsProxy != nil || httpProxy != nil || proxyAutoConfigurationURL != nil || !(proxyBypassDomains?.isEmpty ?? true) else {
|
guard httpsProxy != nil || httpProxy != nil || proxyAutoConfigurationURL != nil || !(proxyBypassDomains?.isEmpty ?? true) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return (httpsProxy ?? httpProxy, proxyAutoConfigurationURL, proxyBypassDomains ?? [])
|
return (httpsProxy ?? httpProxy, proxyAutoConfigurationURL, proxyBypassDomains ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
var otherSettings: (keepAlive: TimeInterval?, reneg: TimeInterval?, randomizeEndpoint: Bool?, randomizeHostnames: Bool?)? {
|
var otherSettings: (keepAlive: TimeInterval?, reneg: TimeInterval?, randomizeEndpoint: Bool?, randomizeHostnames: Bool?)? {
|
||||||
guard keepAliveInterval != nil || renegotiatesAfter != nil || randomizeEndpoint != nil || randomizeHostnames != nil else {
|
guard keepAliveInterval != nil || renegotiatesAfter != nil || randomizeEndpoint != nil || randomizeHostnames != nil else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -361,15 +361,15 @@ private extension EndpointAdvancedView.OpenVPNView {
|
||||||
var fallbackCipher: OpenVPN.Cipher {
|
var fallbackCipher: OpenVPN.Cipher {
|
||||||
fallbackConfiguration.cipher!
|
fallbackConfiguration.cipher!
|
||||||
}
|
}
|
||||||
|
|
||||||
var fallbackDigest: OpenVPN.Digest {
|
var fallbackDigest: OpenVPN.Digest {
|
||||||
fallbackConfiguration.digest!
|
fallbackConfiguration.digest!
|
||||||
}
|
}
|
||||||
|
|
||||||
var fallbackCompressionFraming: OpenVPN.CompressionFraming {
|
var fallbackCompressionFraming: OpenVPN.CompressionFraming {
|
||||||
fallbackConfiguration.compressionFraming!
|
fallbackConfiguration.compressionFraming!
|
||||||
}
|
}
|
||||||
|
|
||||||
var fallbackCompressionAlgorithm: OpenVPN.CompressionAlgorithm {
|
var fallbackCompressionAlgorithm: OpenVPN.CompressionAlgorithm {
|
||||||
fallbackConfiguration.compressionAlgorithm!
|
fallbackConfiguration.compressionAlgorithm!
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ extension EndpointAdvancedView.WireGuardView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mtuSection: some View {
|
private var mtuSection: some View {
|
||||||
builder.mtu.map { mtu in
|
builder.mtu.map { mtu in
|
||||||
Section {
|
Section {
|
||||||
|
|
|
@ -36,21 +36,21 @@ extension EndpointView {
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
@Binding private var builder: OpenVPN.ConfigurationBuilder
|
@Binding private var builder: OpenVPN.ConfigurationBuilder
|
||||||
|
|
||||||
@Binding private var customEndpoint: Endpoint?
|
@Binding private var customEndpoint: Endpoint?
|
||||||
|
|
||||||
private var isConfigurationReadonly: Bool {
|
private var isConfigurationReadonly: Bool {
|
||||||
currentProfile.value.isProvider
|
currentProfile.value.isProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var isFirstAppearance = true
|
@State private var isFirstAppearance = true
|
||||||
|
|
||||||
@State private var isAutomatic = false
|
@State private var isAutomatic = false
|
||||||
|
|
||||||
@State private var selectedSocketType: SocketType = .udp
|
@State private var selectedSocketType: SocketType = .udp
|
||||||
|
|
||||||
@State private var selectedPort: UInt16 = 0
|
@State private var selectedPort: UInt16 = 0
|
||||||
|
|
||||||
// XXX: do not escape mutating 'self', use constant providerManager
|
// XXX: do not escape mutating 'self', use constant providerManager
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
let providerManager: ProviderManager = .shared
|
let providerManager: ProviderManager = .shared
|
||||||
|
@ -104,7 +104,7 @@ extension EndpointView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollViewReader { scrollProxy in
|
ScrollViewReader { scrollProxy in
|
||||||
List {
|
List {
|
||||||
|
@ -135,7 +135,7 @@ extension EndpointView.OpenVPNView {
|
||||||
Toggle(L10n.Global.Strings.automatic, isOn: $isAutomatic.themeAnimation())
|
Toggle(L10n.Global.Strings.automatic, isOn: $isAutomatic.themeAnimation())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var filtersSection: some View {
|
private var filtersSection: some View {
|
||||||
Section {
|
Section {
|
||||||
themeTextPicker(
|
themeTextPicker(
|
||||||
|
@ -162,7 +162,7 @@ extension EndpointView.OpenVPNView {
|
||||||
Text(L10n.Global.Strings.addresses)
|
Text(L10n.Global.Strings.addresses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var advancedSection: some View {
|
private var advancedSection: some View {
|
||||||
Section {
|
Section {
|
||||||
let caption = L10n.Endpoint.Advanced.title
|
let caption = L10n.Endpoint.Advanced.title
|
||||||
|
@ -175,7 +175,7 @@ extension EndpointView.OpenVPNView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func button(forEndpoint endpoint: Endpoint?) -> some View {
|
private func button(forEndpoint endpoint: Endpoint?) -> some View {
|
||||||
Button {
|
Button {
|
||||||
customEndpoint = endpoint
|
customEndpoint = endpoint
|
||||||
|
@ -265,7 +265,7 @@ extension EndpointView.OpenVPNView {
|
||||||
}.map(\.proto.port))
|
}.map(\.proto.port))
|
||||||
return Array(allPorts).sorted()
|
return Array(allPorts).sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var filteredRemotes: [Endpoint]? {
|
private var filteredRemotes: [Endpoint]? {
|
||||||
builder.remotes?.filter {
|
builder.remotes?.filter {
|
||||||
$0.proto.socketType == selectedSocketType && $0.proto.port == selectedPort
|
$0.proto.socketType == selectedSocketType && $0.proto.port == selectedPort
|
||||||
|
|
|
@ -38,7 +38,7 @@ extension EndpointView {
|
||||||
@Binding private var builder: WireGuard.ConfigurationBuilder
|
@Binding private var builder: WireGuard.ConfigurationBuilder
|
||||||
|
|
||||||
// var customPeer: Binding<Endpoint?>? = nil
|
// var customPeer: Binding<Endpoint?>? = nil
|
||||||
|
|
||||||
// XXX: do not escape mutating 'self', use constant providerManager
|
// XXX: do not escape mutating 'self', use constant providerManager
|
||||||
init(currentProfile: ObservableProfile, isReadonly: Bool) {
|
init(currentProfile: ObservableProfile, isReadonly: Bool) {
|
||||||
let providerManager: ProviderManager = .shared
|
let providerManager: ProviderManager = .shared
|
||||||
|
|
|
@ -28,11 +28,11 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct EndpointView: View {
|
struct EndpointView: View {
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch currentProfile.value.currentVPNProtocol {
|
switch currentProfile.value.currentVPNProtocol {
|
||||||
case .openVPN:
|
case .openVPN:
|
||||||
|
|
|
@ -34,7 +34,7 @@ struct NetworkSettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var settings = Profile.NetworkSettings()
|
@State private var settings = Profile.NetworkSettings()
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ struct NetworkSettingsView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditButton()
|
// EditButton()
|
||||||
// .disabled(!isAnythingManual)
|
// .disabled(!isAnythingManual)
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ struct NetworkSettingsView: View {
|
||||||
// }
|
// }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mapNotEmpty(elements: [IdentifiableString]) -> [IdentifiableString] {
|
private func mapNotEmpty(elements: [IdentifiableString]) -> [IdentifiableString] {
|
||||||
elements
|
elements
|
||||||
.filter { !$0.string.isEmpty }
|
.filter { !$0.string.isEmpty }
|
||||||
|
@ -110,7 +110,7 @@ extension NetworkSettingsView {
|
||||||
// MARK: DNS
|
// MARK: DNS
|
||||||
|
|
||||||
extension NetworkSettingsView {
|
extension NetworkSettingsView {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var dnsView: some View {
|
private var dnsView: some View {
|
||||||
Section {
|
Section {
|
||||||
|
@ -146,7 +146,7 @@ extension NetworkSettingsView {
|
||||||
dnsManualDomains
|
dnsManualDomains
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var dnsManualHTTPSRow: some View {
|
private var dnsManualHTTPSRow: some View {
|
||||||
TextField(Unlocalized.Placeholders.dohURL, text: $settings.dns.dnsHTTPSURL.toString())
|
TextField(Unlocalized.Placeholders.dohURL, text: $settings.dns.dnsHTTPSURL.toString())
|
||||||
.themeValidURL(settings.dns.dnsHTTPSURL?.absoluteString)
|
.themeValidURL(settings.dns.dnsHTTPSURL?.absoluteString)
|
||||||
|
@ -177,7 +177,7 @@ extension NetworkSettingsView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var dnsManualDomains: some View {
|
private var dnsManualDomains: some View {
|
||||||
Section {
|
Section {
|
||||||
EditableTextList(
|
EditableTextList(
|
||||||
|
@ -203,7 +203,7 @@ extension NetworkSettingsView {
|
||||||
// MARK: Proxy
|
// MARK: Proxy
|
||||||
|
|
||||||
extension NetworkSettingsView {
|
extension NetworkSettingsView {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var proxyView: some View {
|
private var proxyView: some View {
|
||||||
Section {
|
Section {
|
||||||
|
@ -242,7 +242,7 @@ extension NetworkSettingsView {
|
||||||
proxyManualBypassDomains
|
proxyManualBypassDomains
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var proxyManualBypassDomains: some View {
|
private var proxyManualBypassDomains: some View {
|
||||||
Section {
|
Section {
|
||||||
EditableTextList(
|
EditableTextList(
|
||||||
|
@ -271,7 +271,7 @@ extension NetworkSettingsView {
|
||||||
private var mtuView: some View {
|
private var mtuView: some View {
|
||||||
Section {
|
Section {
|
||||||
Toggle(L10n.Global.Strings.automatic, isOn: $settings.isAutomaticMTU.themeAnimation())
|
Toggle(L10n.Global.Strings.automatic, isOn: $settings.isAutomaticMTU.themeAnimation())
|
||||||
|
|
||||||
if !settings.isAutomaticMTU {
|
if !settings.isAutomaticMTU {
|
||||||
themeTextPicker(
|
themeTextPicker(
|
||||||
L10n.Global.Strings.bytes,
|
L10n.Global.Strings.bytes,
|
||||||
|
|
|
@ -29,9 +29,9 @@ import PassepartoutLibrary
|
||||||
extension OnDemandView {
|
extension OnDemandView {
|
||||||
struct SSIDList: View {
|
struct SSIDList: View {
|
||||||
@Binding var withSSIDs: [String: Bool]
|
@Binding var withSSIDs: [String: Bool]
|
||||||
|
|
||||||
@StateObject private var reader = SSIDReader()
|
@StateObject private var reader = SSIDReader()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
EditableTextList(elements: allSSIDs, allowsDuplicates: false, mapping: mapElements) { text in
|
EditableTextList(elements: allSSIDs, allowsDuplicates: false, mapping: mapElements) { text in
|
||||||
requestSSID(text)
|
requestSSID(text)
|
||||||
|
@ -43,13 +43,13 @@ extension OnDemandView {
|
||||||
Text(L10n.Global.Strings.add)
|
Text(L10n.Global.Strings.add)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mapElements(elements: [IdentifiableString]) -> [IdentifiableString] {
|
private func mapElements(elements: [IdentifiableString]) -> [IdentifiableString] {
|
||||||
elements
|
elements
|
||||||
.filter { !$0.string.isEmpty }
|
.filter { !$0.string.isEmpty }
|
||||||
.sorted { $0.string.lowercased() < $1.string.lowercased() }
|
.sorted { $0.string.lowercased() < $1.string.lowercased() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ssidRow(callback: EditableTextList.FieldCallback) -> some View {
|
private func ssidRow(callback: EditableTextList.FieldCallback) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if callback.isNewElement {
|
if callback.isNewElement {
|
||||||
|
@ -61,7 +61,7 @@ extension OnDemandView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ssidField(callback: EditableTextList.FieldCallback) -> some View {
|
private func ssidField(callback: EditableTextList.FieldCallback) -> some View {
|
||||||
TextField(
|
TextField(
|
||||||
Unlocalized.Network.ssid,
|
Unlocalized.Network.ssid,
|
||||||
|
@ -102,7 +102,7 @@ extension OnDemandView.SSIDList {
|
||||||
// print(">>> withSSIDs (allSSIDs): \(withSSIDs)")
|
// print(">>> withSSIDs (allSSIDs): \(withSSIDs)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var onSSIDs: Binding<Set<String>> {
|
private var onSSIDs: Binding<Set<String>> {
|
||||||
.init {
|
.init {
|
||||||
Set(withSSIDs.filter {
|
Set(withSSIDs.filter {
|
||||||
|
@ -128,7 +128,7 @@ extension OnDemandView.SSIDList {
|
||||||
// print(">>> withSSIDs (onSSIDs): \(withSSIDs)")
|
// print(">>> withSSIDs (onSSIDs): \(withSSIDs)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isSSIDOn(_ ssid: String) -> Binding<Bool> {
|
private func isSSIDOn(_ ssid: String) -> Binding<Bool> {
|
||||||
.init {
|
.init {
|
||||||
withSSIDs[ssid] ?? false
|
withSSIDs[ssid] ?? false
|
||||||
|
|
|
@ -36,7 +36,7 @@ struct OnDemandView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var onDemand = Profile.OnDemand()
|
@State private var onDemand = Profile.OnDemand()
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
|
@ -72,7 +72,7 @@ extension OnDemandView {
|
||||||
Toggle(L10n.Global.Strings.enabled, isOn: $onDemand.isEnabled.themeAnimation())
|
Toggle(L10n.Global.Strings.enabled, isOn: $onDemand.isEnabled.themeAnimation())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
if Utils.hasCellularData() {
|
if Utils.hasCellularData() {
|
||||||
|
@ -118,7 +118,7 @@ extension OnDemandView {
|
||||||
IntentDispatcher.donateTrustCellularNetwork()
|
IntentDispatcher.donateTrustCellularNetwork()
|
||||||
IntentDispatcher.donateUntrustCellularNetwork()
|
IntentDispatcher.donateUntrustCellularNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
// eligibility: donate intents if eligible for Siri
|
// eligibility: donate intents if eligible for Siri
|
||||||
private func donateNetworkIntents(_: [String: Bool]) {
|
private func donateNetworkIntents(_: [String: Bool]) {
|
||||||
guard isEligibleForSiri else {
|
guard isEligibleForSiri else {
|
||||||
|
|
|
@ -29,11 +29,11 @@ import PassepartoutLibrary
|
||||||
extension OrganizerView {
|
extension OrganizerView {
|
||||||
struct ProfileRow: View {
|
struct ProfileRow: View {
|
||||||
private let profile: Profile
|
private let profile: Profile
|
||||||
|
|
||||||
private let isActiveProfile: Bool
|
private let isActiveProfile: Bool
|
||||||
|
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
private var interactiveProfile: Binding<Profile?> {
|
private var interactiveProfile: Binding<Profile?> {
|
||||||
.init {
|
.init {
|
||||||
if case .interactiveAccount(let profile) = modalType {
|
if case .interactiveAccount(let profile) = modalType {
|
||||||
|
@ -48,13 +48,13 @@ extension OrganizerView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(profile: Profile, isActiveProfile: Bool, modalType: Binding<ModalType?>) {
|
init(profile: Profile, isActiveProfile: Bool, modalType: Binding<ModalType?>) {
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
self.isActiveProfile = isActiveProfile
|
self.isActiveProfile = isActiveProfile
|
||||||
_modalType = modalType
|
_modalType = modalType
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return HStack {
|
return HStack {
|
||||||
|
@ -62,7 +62,7 @@ extension OrganizerView {
|
||||||
Text(profile.header.name)
|
Text(profile.header.name)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.themeLongTextStyle()
|
.themeLongTextStyle()
|
||||||
|
|
||||||
VPNStatusText(isActiveProfile: isActiveProfile)
|
VPNStatusText(isActiveProfile: isActiveProfile)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.themeSecondaryTextStyle()
|
.themeSecondaryTextStyle()
|
||||||
|
|
|
@ -29,7 +29,7 @@ import PassepartoutLibrary
|
||||||
extension OrganizerView {
|
extension OrganizerView {
|
||||||
struct ProfilesList: View {
|
struct ProfilesList: View {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
init(modalType: Binding<ModalType?>) {
|
init(modalType: Binding<ModalType?>) {
|
||||||
|
@ -49,7 +49,7 @@ extension OrganizerView {
|
||||||
profileManager.currentProfileId = $0.id
|
profileManager.currentProfileId = $0.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
List {
|
List {
|
||||||
if profileManager.hasProfiles {
|
if profileManager.hasProfiles {
|
||||||
|
@ -69,7 +69,7 @@ extension OrganizerView {
|
||||||
}
|
}
|
||||||
}.themeAnimation(on: profileManager.headers)
|
}.themeAnimation(on: profileManager.headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var profilesView: some View {
|
private var profilesView: some View {
|
||||||
ForEach(sortedProfiles, content: profileRow(forProfile:))
|
ForEach(sortedProfiles, content: profileRow(forProfile:))
|
||||||
.onDelete(perform: removeProfiles)
|
.onDelete(perform: removeProfiles)
|
||||||
|
@ -91,7 +91,7 @@ extension OrganizerView {
|
||||||
ProfileContextMenu(header: profile.header)
|
ProfileContextMenu(header: profile.header)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func profileLabel(forProfile profile: Profile) -> some View {
|
private func profileLabel(forProfile profile: Profile) -> some View {
|
||||||
ProfileRow(
|
ProfileRow(
|
||||||
profile: profile,
|
profile: profile,
|
||||||
|
@ -124,7 +124,7 @@ extension OrganizerView {
|
||||||
profileManager.removeProfiles(withIds: toDelete)
|
profileManager.removeProfiles(withIds: toDelete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performMigrationsIfNeeded() {
|
private func performMigrationsIfNeeded() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
UpgradeManager.shared.doMigrations(profileManager)
|
UpgradeManager.shared.doMigrations(profileManager)
|
||||||
|
@ -140,13 +140,13 @@ extension OrganizerView {
|
||||||
@ObservedObject private var currentVPNState: ObservableVPNState
|
@ObservedObject private var currentVPNState: ObservableVPNState
|
||||||
|
|
||||||
let header: Profile.Header
|
let header: Profile.Header
|
||||||
|
|
||||||
init(header: Profile.Header) {
|
init(header: Profile.Header) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
currentVPNState = .shared
|
currentVPNState = .shared
|
||||||
self.header = header
|
self.header = header
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if #available(iOS 16, *), isActiveProfileNotDisconnected {
|
if #available(iOS 16, *), isActiveProfileNotDisconnected {
|
||||||
reconnectButton
|
reconnectButton
|
||||||
|
@ -154,11 +154,11 @@ extension OrganizerView {
|
||||||
duplicateButton
|
duplicateButton
|
||||||
deleteButton
|
deleteButton
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isActiveProfileNotDisconnected: Bool {
|
private var isActiveProfileNotDisconnected: Bool {
|
||||||
profileManager.isActiveProfile(header.id) && currentVPNState.vpnStatus != .disconnected
|
profileManager.isActiveProfile(header.id) && currentVPNState.vpnStatus != .disconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
private var reconnectButton: some View {
|
private var reconnectButton: some View {
|
||||||
ProfileView.ReconnectButton()
|
ProfileView.ReconnectButton()
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,13 +31,13 @@ extension OrganizerView {
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
@ObservedObject private var vpnManager: VPNManager
|
@ObservedObject private var vpnManager: VPNManager
|
||||||
|
|
||||||
@Binding private var alertType: AlertType?
|
@Binding private var alertType: AlertType?
|
||||||
|
|
||||||
@Binding private var didHandleSubreddit: Bool
|
@Binding private var didHandleSubreddit: Bool
|
||||||
|
|
||||||
@State private var isFirstLaunch = true
|
@State private var isFirstLaunch = true
|
||||||
|
|
||||||
init(alertType: Binding<AlertType?>, didHandleSubreddit: Binding<Bool>) {
|
init(alertType: Binding<AlertType?>, didHandleSubreddit: Binding<Bool>) {
|
||||||
|
@ -46,7 +46,7 @@ extension OrganizerView {
|
||||||
_alertType = alertType
|
_alertType = alertType
|
||||||
_didHandleSubreddit = didHandleSubreddit
|
_didHandleSubreddit = didHandleSubreddit
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
// dummy text, EmptyView() does not trigger on*() handlers
|
// dummy text, EmptyView() does not trigger on*() handlers
|
||||||
|
@ -55,7 +55,7 @@ extension OrganizerView {
|
||||||
.onAppear(perform: onAppear)
|
.onAppear(perform: onAppear)
|
||||||
.onChange(of: scenePhase, perform: onScenePhase)
|
.onChange(of: scenePhase, perform: onScenePhase)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onAppear() {
|
private func onAppear() {
|
||||||
guard didHandleSubreddit else {
|
guard didHandleSubreddit else {
|
||||||
alertType = .subscribeReddit
|
alertType = .subscribeReddit
|
||||||
|
@ -92,7 +92,7 @@ extension OrganizerView {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func persist() {
|
private func persist() {
|
||||||
profileManager.persist()
|
profileManager.persist()
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,9 @@ struct OrganizerView: View {
|
||||||
|
|
||||||
enum AlertType: Identifiable {
|
enum AlertType: Identifiable {
|
||||||
case subscribeReddit
|
case subscribeReddit
|
||||||
|
|
||||||
case error(String, String)
|
case error(String, String)
|
||||||
|
|
||||||
// XXX: alert ids
|
// XXX: alert ids
|
||||||
var id: Int {
|
var id: Int {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -62,11 +62,11 @@ struct OrganizerView: View {
|
||||||
@State private var isHostFileImporterPresented = false
|
@State private var isHostFileImporterPresented = false
|
||||||
|
|
||||||
@AppStorage(AppPreference.didHandleSubreddit.key) private var didHandleSubreddit = false
|
@AppStorage(AppPreference.didHandleSubreddit.key) private var didHandleSubreddit = false
|
||||||
|
|
||||||
private let hostFileTypes = Constants.URLs.filetypes
|
private let hostFileTypes = Constants.URLs.filetypes
|
||||||
|
|
||||||
private let redditURL = Constants.URLs.subreddit
|
private let redditURL = Constants.URLs.subreddit
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return ZStack {
|
return ZStack {
|
||||||
|
@ -93,13 +93,13 @@ struct OrganizerView: View {
|
||||||
onCompletion: onHostFileImporterResult
|
onCompletion: onHostFileImporterResult
|
||||||
).onOpenURL(perform: onOpenURL)
|
).onOpenURL(perform: onOpenURL)
|
||||||
.themePrimaryView()
|
.themePrimaryView()
|
||||||
|
|
||||||
// VPN configuration error publisher (no need to observe VPNManager)
|
// VPN configuration error publisher (no need to observe VPNManager)
|
||||||
.onReceive(VPNManager.shared.configurationError) {
|
.onReceive(VPNManager.shared.configurationError) {
|
||||||
alertType = .error($0.profile.header.name, $0.error.localizedAppDescription)
|
alertType = .error($0.profile.header.name, $0.error.localizedAppDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hiddenSceneView: some View {
|
private var hiddenSceneView: some View {
|
||||||
SceneView(
|
SceneView(
|
||||||
alertType: $alertType,
|
alertType: $alertType,
|
||||||
|
@ -128,7 +128,7 @@ extension OrganizerView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onOpenURL(_ url: URL) {
|
private func onOpenURL(_ url: URL) {
|
||||||
addProfileModalType = .addHost(url, false)
|
addProfileModalType = .addHost(url, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ extension PaywallView {
|
||||||
var id: Int {
|
var id: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case .purchaseFailed: return 1
|
case .purchaseFailed: return 1
|
||||||
|
|
||||||
case .restoreFailed: return 2
|
case .restoreFailed: return 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,19 +48,19 @@ extension PaywallView {
|
||||||
|
|
||||||
case restoring
|
case restoring
|
||||||
}
|
}
|
||||||
|
|
||||||
private typealias RowModel = (product: SKProduct, extra: String?)
|
private typealias RowModel = (product: SKProduct, extra: String?)
|
||||||
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
@Binding private var isPresented: Bool
|
@Binding private var isPresented: Bool
|
||||||
|
|
||||||
private let feature: LocalProduct?
|
private let feature: LocalProduct?
|
||||||
|
|
||||||
@State private var alertType: AlertType?
|
@State private var alertType: AlertType?
|
||||||
|
|
||||||
@State private var purchaseState: PurchaseState?
|
@State private var purchaseState: PurchaseState?
|
||||||
|
|
||||||
init(isPresented: Binding<Bool>, feature: LocalProduct? = nil) {
|
init(isPresented: Binding<Bool>, feature: LocalProduct? = nil) {
|
||||||
|
@ -68,7 +68,7 @@ extension PaywallView {
|
||||||
_isPresented = isPresented
|
_isPresented = isPresented
|
||||||
self.feature = feature
|
self.feature = feature
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
productsSection
|
productsSection
|
||||||
|
@ -85,7 +85,7 @@ extension PaywallView {
|
||||||
}
|
}
|
||||||
}.themeAnimation(on: productManager.isRefreshingProducts)
|
}.themeAnimation(on: productManager.isRefreshingProducts)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
||||||
switch alertType {
|
switch alertType {
|
||||||
case .purchaseFailed(let product, let error):
|
case .purchaseFailed(let product, let error):
|
||||||
|
@ -96,7 +96,7 @@ extension PaywallView {
|
||||||
purchaseState = nil
|
purchaseState = nil
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
case .restoreFailed(let error):
|
case .restoreFailed(let error):
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text(L10n.Paywall.Items.Restore.title),
|
title: Text(L10n.Paywall.Items.Restore.title),
|
||||||
|
@ -107,7 +107,7 @@ extension PaywallView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var productsSection: some View {
|
private var productsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
if !productManager.isRefreshingProducts {
|
if !productManager.isRefreshingProducts {
|
||||||
|
@ -134,7 +134,7 @@ extension PaywallView {
|
||||||
purchaseState: purchaseState
|
purchaseState: purchaseState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var restoreRow: some View {
|
private var restoreRow: some View {
|
||||||
PurchaseRow(
|
PurchaseRow(
|
||||||
title: L10n.Paywall.Items.Restore.title,
|
title: L10n.Paywall.Items.Restore.title,
|
||||||
|
@ -156,7 +156,7 @@ extension PaywallView.PurchaseView {
|
||||||
switch result {
|
switch result {
|
||||||
case .done:
|
case .done:
|
||||||
isPresented = false
|
isPresented = false
|
||||||
|
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,7 @@ extension PaywallView.PurchaseView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func restorePurchases() {
|
private func restorePurchases() {
|
||||||
purchaseState = .restoring
|
purchaseState = .restoring
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ extension PaywallView.PurchaseView {
|
||||||
}
|
}
|
||||||
return productManager.product(withIdentifier: feature)
|
return productManager.product(withIdentifier: feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var skPlatformVersion: SKProduct? {
|
private var skPlatformVersion: SKProduct? {
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
productManager.product(withIdentifier: .fullVersion_macOS)
|
productManager.product(withIdentifier: .fullVersion_macOS)
|
||||||
|
@ -258,13 +258,13 @@ private struct PurchaseRow: View {
|
||||||
var product: SKProduct?
|
var product: SKProduct?
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
|
|
||||||
let extra: String?
|
let extra: String?
|
||||||
|
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
let purchaseState: PaywallView.PurchaseView.PurchaseState?
|
let purchaseState: PaywallView.PurchaseView.PurchaseState?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
actionButton
|
actionButton
|
||||||
|
@ -276,7 +276,7 @@ private struct PurchaseRow: View {
|
||||||
}
|
}
|
||||||
}.padding([.top, .bottom])
|
}.padding([.top, .bottom])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var actionButton: some View {
|
private var actionButton: some View {
|
||||||
if let product = product {
|
if let product = product {
|
||||||
|
@ -285,7 +285,7 @@ private struct PurchaseRow: View {
|
||||||
restoreButton
|
restoreButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func purchaseButton(_ product: SKProduct) -> some View {
|
private func purchaseButton(_ product: SKProduct) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Button(title, action: action)
|
Button(title, action: action)
|
||||||
|
@ -300,7 +300,7 @@ private struct PurchaseRow: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var restoreButton: some View {
|
private var restoreButton: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Button(title, action: action)
|
Button(title, action: action)
|
||||||
|
|
|
@ -27,9 +27,9 @@ import SwiftUI
|
||||||
|
|
||||||
struct PaywallView: View {
|
struct PaywallView: View {
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
@Binding private var isPresented: Bool
|
@Binding private var isPresented: Bool
|
||||||
|
|
||||||
private let feature: LocalProduct?
|
private let feature: LocalProduct?
|
||||||
|
|
||||||
init<MT>(modalType: Binding<MT?>, feature: LocalProduct? = nil) {
|
init<MT>(modalType: Binding<MT?>, feature: LocalProduct? = nil) {
|
||||||
|
|
|
@ -33,15 +33,15 @@ extension ProfileView {
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
private var isEligibleForNetworkSettings: Bool {
|
private var isEligibleForNetworkSettings: Bool {
|
||||||
productManager.isEligible(forFeature: .networkSettings)
|
productManager.isEligible(forFeature: .networkSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isEligibleForTrustedNetworks: Bool {
|
private var isEligibleForTrustedNetworks: Bool {
|
||||||
productManager.isEligible(forFeature: .trustedNetworks)
|
productManager.isEligible(forFeature: .trustedNetworks)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
|
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
|
@ -111,7 +111,7 @@ extension ProfileView {
|
||||||
Text(L10n.Global.Strings.configuration)
|
Text(L10n.Global.Strings.configuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var networkSettingsRow: some View {
|
private var networkSettingsRow: some View {
|
||||||
Label(L10n.NetworkSettings.title, systemImage: themeNetworkSettingsImage)
|
Label(L10n.NetworkSettings.title, systemImage: themeNetworkSettingsImage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import PassepartoutLibrary
|
||||||
extension ProfileView {
|
extension ProfileView {
|
||||||
struct DiagnosticsSection: View {
|
struct DiagnosticsSection: View {
|
||||||
@ObservedObject var currentProfile: ObservableProfile
|
@ObservedObject var currentProfile: ObservableProfile
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
|
|
|
@ -29,11 +29,11 @@ import PassepartoutLibrary
|
||||||
extension ProfileView {
|
extension ProfileView {
|
||||||
struct ExtraSection: View {
|
struct ExtraSection: View {
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if currentProfile.value.isProvider {
|
if currentProfile.value.isProvider {
|
||||||
Section {
|
Section {
|
||||||
|
|
|
@ -30,36 +30,36 @@ extension ProfileView {
|
||||||
struct MainMenu: View {
|
struct MainMenu: View {
|
||||||
enum ActionSheetType: Int, Identifiable {
|
enum ActionSheetType: Int, Identifiable {
|
||||||
case uninstallVPN
|
case uninstallVPN
|
||||||
|
|
||||||
case deleteProfile
|
case deleteProfile
|
||||||
|
|
||||||
var id: Int {
|
var id: Int {
|
||||||
rawValue
|
rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
@ObservedObject private var vpnManager: VPNManager
|
@ObservedObject private var vpnManager: VPNManager
|
||||||
|
|
||||||
@ObservedObject private var currentVPNState: ObservableVPNState
|
@ObservedObject private var currentVPNState: ObservableVPNState
|
||||||
|
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
private var header: Profile.Header {
|
private var header: Profile.Header {
|
||||||
currentProfile.value.header
|
currentProfile.value.header
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
@State private var actionSheetType: ActionSheetType?
|
@State private var actionSheetType: ActionSheetType?
|
||||||
|
|
||||||
private let uninstallVPNTitle = L10n.Global.Strings.uninstall
|
private let uninstallVPNTitle = L10n.Global.Strings.uninstall
|
||||||
|
|
||||||
private let deleteProfileTitle = L10n.Global.Strings.delete
|
private let deleteProfileTitle = L10n.Global.Strings.delete
|
||||||
|
|
||||||
private let cancelTitle = L10n.Global.Strings.cancel
|
private let cancelTitle = L10n.Global.Strings.cancel
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
|
init(currentProfile: ObservableProfile, modalType: Binding<ModalType?>) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
vpnManager = .shared
|
vpnManager = .shared
|
||||||
|
@ -79,7 +79,7 @@ extension ProfileView {
|
||||||
primaryButton: .destructive(Text(uninstallVPNTitle), action: uninstallVPN),
|
primaryButton: .destructive(Text(uninstallVPNTitle), action: uninstallVPN),
|
||||||
secondaryButton: .cancel(Text(cancelTitle))
|
secondaryButton: .cancel(Text(cancelTitle))
|
||||||
)
|
)
|
||||||
|
|
||||||
case .deleteProfile:
|
case .deleteProfile:
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text(deleteProfileTitle),
|
title: Text(deleteProfileTitle),
|
||||||
|
@ -114,7 +114,7 @@ extension ProfileView {
|
||||||
themeSettingsMenuImage.asSystemImage
|
themeSettingsMenuImage.asSystemImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isActiveProfileNotDisconnected: Bool {
|
private var isActiveProfileNotDisconnected: Bool {
|
||||||
profileManager.isActiveProfile(header.id) && currentVPNState.vpnStatus != .disconnected
|
profileManager.isActiveProfile(header.id) && currentVPNState.vpnStatus != .disconnected
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ extension ProfileView {
|
||||||
Label(uninstallVPNTitle, systemImage: themeUninstallImage)
|
Label(uninstallVPNTitle, systemImage: themeUninstallImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var deleteProfileButton: some View {
|
private var deleteProfileButton: some View {
|
||||||
DestructiveButton {
|
DestructiveButton {
|
||||||
actionSheetType = .deleteProfile
|
actionSheetType = .deleteProfile
|
||||||
|
@ -152,11 +152,11 @@ extension ProfileView {
|
||||||
extension ProfileView {
|
extension ProfileView {
|
||||||
struct ReconnectButton: View {
|
struct ReconnectButton: View {
|
||||||
@ObservedObject private var vpnManager: VPNManager
|
@ObservedObject private var vpnManager: VPNManager
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
vpnManager = .shared
|
vpnManager = .shared
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
@ -167,12 +167,12 @@ extension ProfileView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ShortcutsButton: View {
|
struct ShortcutsButton: View {
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
init(modalType: Binding<ModalType?>) {
|
init(modalType: Binding<ModalType?>) {
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
_modalType = modalType
|
_modalType = modalType
|
||||||
|
@ -181,7 +181,7 @@ extension ProfileView {
|
||||||
private var isEligibleForSiri: Bool {
|
private var isEligibleForSiri: Bool {
|
||||||
productManager.isEligible(forFeature: .siriShortcuts)
|
productManager.isEligible(forFeature: .siriShortcuts)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
presentShortcutsOrPaywall()
|
presentShortcutsOrPaywall()
|
||||||
|
@ -200,14 +200,14 @@ extension ProfileView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RenameButton: View {
|
struct RenameButton: View {
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
init(modalType: Binding<ModalType?>) {
|
init(modalType: Binding<ModalType?>) {
|
||||||
_modalType = modalType
|
_modalType = modalType
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
modalType = .rename
|
modalType = .rename
|
||||||
|
@ -219,17 +219,17 @@ extension ProfileView {
|
||||||
|
|
||||||
struct DuplicateButton: View {
|
struct DuplicateButton: View {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
private let header: Profile.Header
|
private let header: Profile.Header
|
||||||
|
|
||||||
private let setAsCurrent: Bool
|
private let setAsCurrent: Bool
|
||||||
|
|
||||||
init(header: Profile.Header, setAsCurrent: Bool) {
|
init(header: Profile.Header, setAsCurrent: Bool) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
self.header = header
|
self.header = header
|
||||||
self.setAsCurrent = setAsCurrent
|
self.setAsCurrent = setAsCurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
duplicateProfile(withId: header.id)
|
duplicateProfile(withId: header.id)
|
||||||
|
|
|
@ -29,17 +29,17 @@ import PassepartoutLibrary
|
||||||
extension ProfileView {
|
extension ProfileView {
|
||||||
struct ProviderSection: View, ProviderProfileAvailability {
|
struct ProviderSection: View, ProviderProfileAvailability {
|
||||||
@ObservedObject var providerManager: ProviderManager
|
@ObservedObject var providerManager: ProviderManager
|
||||||
|
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
var profile: Profile {
|
var profile: Profile {
|
||||||
currentProfile.value
|
currentProfile.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var isProviderLocationPresented = false
|
@State private var isProviderLocationPresented = false
|
||||||
|
|
||||||
@State private var isRefreshingInfrastructure = false
|
@State private var isRefreshingInfrastructure = false
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
providerManager = .shared
|
providerManager = .shared
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
|
@ -55,7 +55,7 @@ extension ProfileView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
Section {
|
Section {
|
||||||
|
@ -116,11 +116,11 @@ extension ProfileView {
|
||||||
}
|
}
|
||||||
return themeAssetsCountryImage(code).asAssetImage
|
return themeAssetsCountryImage(code).asAssetImage
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentProviderPreset: String? {
|
private var currentProviderPreset: String? {
|
||||||
providerManager.localizedPreset(forProfile: profile)
|
providerManager.localizedPreset(forProfile: profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var lastInfrastructureUpdate: String? {
|
private var lastInfrastructureUpdate: String? {
|
||||||
providerManager.localizedInfrastructureUpdate(forProfile: profile)
|
providerManager.localizedInfrastructureUpdate(forProfile: profile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,20 +29,20 @@ import PassepartoutLibrary
|
||||||
extension ProfileView {
|
extension ProfileView {
|
||||||
struct RenameView: View {
|
struct RenameView: View {
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
@State private var newName = ""
|
@State private var newName = ""
|
||||||
|
|
||||||
@State private var isOverwritingExistingProfile = false
|
@State private var isOverwritingExistingProfile = false
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
|
@ -61,7 +61,7 @@ extension ProfileView {
|
||||||
}
|
}
|
||||||
}.alert(isPresented: $isOverwritingExistingProfile, content: alertOverwriteExistingProfile)
|
}.alert(isPresented: $isOverwritingExistingProfile, content: alertOverwriteExistingProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func alertOverwriteExistingProfile() -> Alert {
|
private func alertOverwriteExistingProfile() -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text(L10n.Profile.Alerts.Rename.title),
|
title: Text(L10n.Profile.Alerts.Rename.title),
|
||||||
|
@ -72,7 +72,7 @@ extension ProfileView {
|
||||||
secondaryButton: .cancel(Text(L10n.Global.Strings.cancel))
|
secondaryButton: .cancel(Text(L10n.Global.Strings.cancel))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadCurrentName() {
|
private func loadCurrentName() {
|
||||||
newName = currentProfile.value.header.name
|
newName = currentProfile.value.header.name
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ extension ProfileView {
|
||||||
private let profile: Profile
|
private let profile: Profile
|
||||||
|
|
||||||
@Binding private var modalType: ModalType?
|
@Binding private var modalType: ModalType?
|
||||||
|
|
||||||
private var interactiveProfile: Binding<Profile?> {
|
private var interactiveProfile: Binding<Profile?> {
|
||||||
.init {
|
.init {
|
||||||
modalType == .interactiveAccount ? profile : nil
|
modalType == .interactiveAccount ? profile : nil
|
||||||
|
@ -41,7 +41,7 @@ extension ProfileView {
|
||||||
modalType = $0 != nil ? .interactiveAccount : nil
|
modalType = $0 != nil ? .interactiveAccount : nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isActiveProfile: Bool {
|
private var isActiveProfile: Bool {
|
||||||
profileManager.isActiveProfile(profile.id)
|
profileManager.isActiveProfile(profile.id)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ extension ProfileView {
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
_modalType = modalType
|
_modalType = modalType
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
toggleView
|
toggleView
|
||||||
|
@ -63,7 +63,7 @@ extension ProfileView {
|
||||||
.xxxThemeTruncation()
|
.xxxThemeTruncation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var toggleView: some View {
|
private var toggleView: some View {
|
||||||
VPNToggle(
|
VPNToggle(
|
||||||
profile: profile,
|
profile: profile,
|
||||||
|
@ -71,7 +71,7 @@ extension ProfileView {
|
||||||
rateLimit: Constants.RateLimit.vpnToggle
|
rateLimit: Constants.RateLimit.vpnToggle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var statusView: some View {
|
private var statusView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text(L10n.Profile.Items.ConnectionStatus.caption)
|
Text(L10n.Profile.Items.ConnectionStatus.caption)
|
||||||
|
|
|
@ -31,13 +31,13 @@ struct ProfileView: View {
|
||||||
case interactiveAccount
|
case interactiveAccount
|
||||||
|
|
||||||
case shortcuts
|
case shortcuts
|
||||||
|
|
||||||
case rename
|
case rename
|
||||||
|
|
||||||
case paywallShortcuts
|
case paywallShortcuts
|
||||||
|
|
||||||
case paywallNetworkSettings
|
case paywallNetworkSettings
|
||||||
|
|
||||||
case paywallTrustedNetworks
|
case paywallTrustedNetworks
|
||||||
|
|
||||||
var id: Int {
|
var id: Int {
|
||||||
|
@ -46,17 +46,17 @@ struct ProfileView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
private var isLoading: Bool {
|
private var isLoading: Bool {
|
||||||
currentProfile.isLoading
|
currentProfile.isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isExisting: Bool {
|
private var isExisting: Bool {
|
||||||
!currentProfile.value.isPlaceholder
|
!currentProfile.value.isPlaceholder
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var modalType: ModalType?
|
@State private var modalType: ModalType?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
currentProfile = ProfileManager.shared.currentProfile
|
currentProfile = ProfileManager.shared.currentProfile
|
||||||
}
|
}
|
||||||
|
@ -83,11 +83,11 @@ struct ProfileView: View {
|
||||||
.navigationTitle(title)
|
.navigationTitle(title)
|
||||||
.themeSecondaryView()
|
.themeSecondaryView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var title: String {
|
private var title: String {
|
||||||
currentProfile.name
|
currentProfile.name
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
List {
|
List {
|
||||||
if !isLoading {
|
if !isLoading {
|
||||||
|
@ -107,7 +107,7 @@ struct ProfileView: View {
|
||||||
}
|
}
|
||||||
}.themeAnimation(on: isLoading)
|
}.themeAnimation(on: isLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func presentedModal(_ modalType: ModalType) -> some View {
|
private func presentedModal(_ modalType: ModalType) -> some View {
|
||||||
switch modalType {
|
switch modalType {
|
||||||
|
@ -115,17 +115,17 @@ struct ProfileView: View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
InteractiveConnectionView(profile: currentProfile.value)
|
InteractiveConnectionView(profile: currentProfile.value)
|
||||||
}.themeGlobal()
|
}.themeGlobal()
|
||||||
|
|
||||||
case .shortcuts:
|
case .shortcuts:
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ShortcutsView(target: currentProfile.value)
|
ShortcutsView(target: currentProfile.value)
|
||||||
}.themeGlobal()
|
}.themeGlobal()
|
||||||
|
|
||||||
case .rename:
|
case .rename:
|
||||||
NavigationView {
|
NavigationView {
|
||||||
RenameView(currentProfile: currentProfile)
|
RenameView(currentProfile: currentProfile)
|
||||||
}.themeGlobal()
|
}.themeGlobal()
|
||||||
|
|
||||||
case .paywallShortcuts:
|
case .paywallShortcuts:
|
||||||
NavigationView {
|
NavigationView {
|
||||||
PaywallView(
|
PaywallView(
|
||||||
|
|
|
@ -30,13 +30,13 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
@ObservedObject var providerManager: ProviderManager
|
@ObservedObject var providerManager: ProviderManager
|
||||||
|
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
var profile: Profile {
|
var profile: Profile {
|
||||||
currentProfile.value
|
currentProfile.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private let isEditable: Bool
|
private let isEditable: Bool
|
||||||
|
|
||||||
private var providerName: ProviderName {
|
private var providerName: ProviderName {
|
||||||
guard let name = currentProfile.value.header.providerName else {
|
guard let name = currentProfile.value.header.providerName else {
|
||||||
assertionFailure("Not a provider")
|
assertionFailure("Not a provider")
|
||||||
|
@ -44,15 +44,15 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
private var vpnProtocol: VPNProtocolType {
|
private var vpnProtocol: VPNProtocolType {
|
||||||
currentProfile.value.currentVPNProtocol
|
currentProfile.value.currentVPNProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binding private var selectedServer: ProviderServer?
|
@Binding private var selectedServer: ProviderServer?
|
||||||
|
|
||||||
@Binding private var favoriteLocationIds: Set<String>?
|
@Binding private var favoriteLocationIds: Set<String>?
|
||||||
|
|
||||||
@AppStorage(AppPreference.isShowingFavorites.key) private var isShowingFavorites = false
|
@AppStorage(AppPreference.isShowingFavorites.key) private var isShowingFavorites = false
|
||||||
|
|
||||||
private var isShowingEmptyFavorites: Bool {
|
private var isShowingEmptyFavorites: Bool {
|
||||||
|
@ -61,7 +61,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
}
|
}
|
||||||
return favoriteLocationIds?.isEmpty ?? true
|
return favoriteLocationIds?.isEmpty ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: do not escape mutating 'self', use constant providerManager
|
// XXX: do not escape mutating 'self', use constant providerManager
|
||||||
init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding<Bool>) {
|
init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding<Bool>) {
|
||||||
let providerManager: ProviderManager = .shared
|
let providerManager: ProviderManager = .shared
|
||||||
|
@ -69,7 +69,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
self.providerManager = providerManager
|
self.providerManager = providerManager
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
self.isEditable = isEditable
|
self.isEditable = isEditable
|
||||||
|
|
||||||
_selectedServer = .init {
|
_selectedServer = .init {
|
||||||
guard let serverId = currentProfile.value.providerServerId() else {
|
guard let serverId = currentProfile.value.providerServerId() else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -89,7 +89,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
currentProfile.value.setProviderFavoriteLocationIds($0)
|
currentProfile.value.setProviderFavoriteLocationIds($0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
debugChanges()
|
debugChanges()
|
||||||
return Group {
|
return Group {
|
||||||
|
@ -110,7 +110,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
}
|
}
|
||||||
}.navigationTitle(L10n.Provider.Location.title)
|
}.navigationTitle(L10n.Provider.Location.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainView: some View {
|
private var mainView: some View {
|
||||||
// FIXME: layout, restore ScrollViewReader, but content inside it is not re-rendered on isShowingFavorites
|
// FIXME: layout, restore ScrollViewReader, but content inside it is not re-rendered on isShowingFavorites
|
||||||
// ScrollViewReader { scrollProxy in
|
// ScrollViewReader { scrollProxy in
|
||||||
|
@ -129,7 +129,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
private var categoriesView: some View {
|
private var categoriesView: some View {
|
||||||
ForEach(categories, content: categorySection)
|
ForEach(categories, content: categorySection)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func categorySection(_ category: ProviderCategory) -> some View {
|
private func categorySection(_ category: ProviderCategory) -> some View {
|
||||||
Section {
|
Section {
|
||||||
ForEach(filteredLocations(for: category)) { location in
|
ForEach(filteredLocations(for: category)) { location in
|
||||||
|
@ -146,7 +146,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
!category.name.isEmpty ? Text(category.name) : nil
|
!category.name.isEmpty ? Text(category.name) : nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func locationRow(_ location: ProviderLocation) -> some View {
|
private func locationRow(_ location: ProviderLocation) -> some View {
|
||||||
if let onlyServer = location.onlyServer {
|
if let onlyServer = location.onlyServer {
|
||||||
|
@ -155,7 +155,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
multipleServersRow(location)
|
multipleServersRow(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func multipleServersRow(_ location: ProviderLocation) -> some View {
|
private func multipleServersRow(_ location: ProviderLocation) -> some View {
|
||||||
NavigationLink(destination: {
|
NavigationLink(destination: {
|
||||||
ServerListView(
|
ServerListView(
|
||||||
|
@ -180,7 +180,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var emptyFavoritesSection: some View {
|
private var emptyFavoritesSection: some View {
|
||||||
Section {
|
Section {
|
||||||
} footer: {
|
} footer: {
|
||||||
|
@ -204,7 +204,7 @@ extension ProviderLocationView {
|
||||||
private func server(withId serverId: String) -> ProviderServer? {
|
private func server(withId serverId: String) -> ProviderServer? {
|
||||||
providerManager.server(withId: serverId)
|
providerManager.server(withId: serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var categories: [ProviderCategory] {
|
private var categories: [ProviderCategory] {
|
||||||
providerManager.categories(providerName, vpnProtocol: vpnProtocol)
|
providerManager.categories(providerName, vpnProtocol: vpnProtocol)
|
||||||
.filter {
|
.filter {
|
||||||
|
@ -223,11 +223,11 @@ extension ProviderLocationView {
|
||||||
}
|
}
|
||||||
return locations.sorted()
|
return locations.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isFavoriteLocation(_ location: ProviderLocation) -> Bool {
|
private func isFavoriteLocation(_ location: ProviderLocation) -> Bool {
|
||||||
favoriteLocationIds?.contains(location.id) ?? false
|
favoriteLocationIds?.contains(location.id) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleFavoriteLocation(_ location: ProviderLocation) {
|
private func toggleFavoriteLocation(_ location: ProviderLocation) {
|
||||||
if !isFavoriteLocation(location) {
|
if !isFavoriteLocation(location) {
|
||||||
if favoriteLocationIds == nil {
|
if favoriteLocationIds == nil {
|
||||||
|
@ -248,7 +248,7 @@ extension ProviderLocationView {
|
||||||
let location: ProviderLocation
|
let location: ProviderLocation
|
||||||
|
|
||||||
let selectedLocationId: String?
|
let selectedLocationId: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
themeAssetsCountryImage(location.countryCode).asAssetImage
|
themeAssetsCountryImage(location.countryCode).asAssetImage
|
||||||
|
@ -269,11 +269,11 @@ extension ProviderLocationView {
|
||||||
|
|
||||||
struct ServerListView: View {
|
struct ServerListView: View {
|
||||||
@ObservedObject private var providerManager: ProviderManager
|
@ObservedObject private var providerManager: ProviderManager
|
||||||
|
|
||||||
private let location: ProviderLocation
|
private let location: ProviderLocation
|
||||||
|
|
||||||
@Binding private var selectedServer: ProviderServer?
|
@Binding private var selectedServer: ProviderServer?
|
||||||
|
|
||||||
init(location: ProviderLocation, selectedServer: Binding<ProviderServer?>) {
|
init(location: ProviderLocation, selectedServer: Binding<ProviderServer?>) {
|
||||||
providerManager = .shared
|
providerManager = .shared
|
||||||
self.location = location
|
self.location = location
|
||||||
|
|
|
@ -34,13 +34,13 @@ struct ProviderPresetView: View {
|
||||||
@ObservedObject private var currentProfile: ObservableProfile
|
@ObservedObject private var currentProfile: ObservableProfile
|
||||||
|
|
||||||
private var server: ProviderServer?
|
private var server: ProviderServer?
|
||||||
|
|
||||||
@Binding private var selectedPreset: ProviderServer.Preset?
|
@Binding private var selectedPreset: ProviderServer.Preset?
|
||||||
|
|
||||||
// XXX: do not escape mutating 'self', use constant providerManager
|
// XXX: do not escape mutating 'self', use constant providerManager
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
let providerManager: ProviderManager = .shared
|
let providerManager: ProviderManager = .shared
|
||||||
|
|
||||||
self.providerManager = providerManager
|
self.providerManager = providerManager
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ struct ProviderPresetView: View {
|
||||||
currentProfile.value.setProviderPreset(preset)
|
currentProfile.value.setProviderPreset(preset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(availablePresets, id: \.id, content: presetSection)
|
ForEach(availablePresets, id: \.id, content: presetSection)
|
||||||
|
@ -77,7 +77,7 @@ struct ProviderPresetView: View {
|
||||||
presetSelectionRow(preset)
|
presetSelectionRow(preset)
|
||||||
}
|
}
|
||||||
NavigationLink(L10n.Endpoint.Advanced.title) {
|
NavigationLink(L10n.Endpoint.Advanced.title) {
|
||||||
|
|
||||||
// TODO: WireGuard, preset assumes OpenVPN (read current protocol instead)
|
// TODO: WireGuard, preset assumes OpenVPN (read current protocol instead)
|
||||||
preset.openVPNConfiguration.map {
|
preset.openVPNConfiguration.map {
|
||||||
EndpointAdvancedView.OpenVPNView(
|
EndpointAdvancedView.OpenVPNView(
|
||||||
|
@ -91,7 +91,7 @@ struct ProviderPresetView: View {
|
||||||
Text(preset.name)
|
Text(preset.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presetSelectionRow(_ preset: ProviderServer.Preset) -> some View {
|
private func presetSelectionRow(_ preset: ProviderServer.Preset) -> some View {
|
||||||
Text(preset.comment)
|
Text(preset.comment)
|
||||||
.withTrailingCheckmark(when: preset.id == selectedPreset?.id)
|
.withTrailingCheckmark(when: preset.id == selectedPreset?.id)
|
||||||
|
|
|
@ -29,15 +29,15 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct ReportIssueView: View {
|
struct ReportIssueView: View {
|
||||||
@Binding private var isPresented: Bool
|
@Binding private var isPresented: Bool
|
||||||
|
|
||||||
private let toRecipients: [String]
|
private let toRecipients: [String]
|
||||||
|
|
||||||
private let subject: String
|
private let subject: String
|
||||||
|
|
||||||
private let messageBody: String
|
private let messageBody: String
|
||||||
|
|
||||||
private let attachments: [MailComposerView.Attachment]
|
private let attachments: [MailComposerView.Attachment]
|
||||||
|
|
||||||
init(
|
init(
|
||||||
isPresented: Binding<Bool>,
|
isPresented: Binding<Bool>,
|
||||||
vpnProtocol: VPNProtocolType,
|
vpnProtocol: VPNProtocolType,
|
||||||
|
@ -46,7 +46,7 @@ struct ReportIssueView: View {
|
||||||
lastUpdate: Date? = nil
|
lastUpdate: Date? = nil
|
||||||
) {
|
) {
|
||||||
_isPresented = isPresented
|
_isPresented = isPresented
|
||||||
|
|
||||||
toRecipients = [Unlocalized.Issues.recipient]
|
toRecipients = [Unlocalized.Issues.recipient]
|
||||||
subject = Unlocalized.Issues.subject
|
subject = Unlocalized.Issues.subject
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ struct ReportIssueView: View {
|
||||||
}
|
}
|
||||||
self.attachments = attachments
|
self.attachments = attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
MailComposerView(
|
MailComposerView(
|
||||||
isPresented: $isPresented,
|
isPresented: $isPresented,
|
||||||
|
|
|
@ -30,9 +30,9 @@ struct SettingsView: View {
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
// private var isTestBuild: Bool {
|
// private var isTestBuild: Bool {
|
||||||
// Constants.App.isBeta || Constants.InApp.appType == .beta
|
// Constants.App.isBeta || Constants.InApp.appType == .beta
|
||||||
// }
|
// }
|
||||||
|
@ -45,7 +45,7 @@ struct SettingsView: View {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
productManager = .shared
|
productManager = .shared
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
aboutSection
|
aboutSection
|
||||||
|
@ -54,7 +54,7 @@ struct SettingsView: View {
|
||||||
}.themeSecondaryView()
|
}.themeSecondaryView()
|
||||||
.navigationTitle(L10n.Settings.title)
|
.navigationTitle(L10n.Settings.title)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var aboutSection: some View {
|
private var aboutSection: some View {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
|
|
|
@ -30,11 +30,11 @@ import PassepartoutLibrary
|
||||||
extension ShortcutsView {
|
extension ShortcutsView {
|
||||||
struct AddView: View {
|
struct AddView: View {
|
||||||
@ObservedObject private var providerManager: ProviderManager
|
@ObservedObject private var providerManager: ProviderManager
|
||||||
|
|
||||||
@StateObject private var pendingProfile = ObservableProfile()
|
@StateObject private var pendingProfile = ObservableProfile()
|
||||||
|
|
||||||
private let target: Profile
|
private let target: Profile
|
||||||
|
|
||||||
@Binding private var pendingShortcut: INShortcut?
|
@Binding private var pendingShortcut: INShortcut?
|
||||||
|
|
||||||
@State private var isPresentingProviderLocation = false
|
@State private var isPresentingProviderLocation = false
|
||||||
|
@ -44,7 +44,7 @@ extension ShortcutsView {
|
||||||
self.target = target
|
self.target = target
|
||||||
_pendingShortcut = pendingShortcut
|
_pendingShortcut = pendingShortcut
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
hiddenProviderLocationLink
|
hiddenProviderLocationLink
|
||||||
|
@ -82,7 +82,7 @@ extension ShortcutsView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hiddenProviderLocationLink: some View {
|
private var hiddenProviderLocationLink: some View {
|
||||||
NavigationLink("", isActive: $isPresentingProviderLocation) {
|
NavigationLink("", isActive: $isPresentingProviderLocation) {
|
||||||
ProviderLocationView(
|
ProviderLocationView(
|
||||||
|
@ -106,7 +106,7 @@ extension ShortcutsView.AddView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addConnect(_ header: Profile.Header) {
|
private func addConnect(_ header: Profile.Header) {
|
||||||
pendingShortcut = INShortcut(intent: IntentDispatcher.intentConnect(
|
pendingShortcut = INShortcut(intent: IntentDispatcher.intentConnect(
|
||||||
header: header
|
header: header
|
||||||
|
|
|
@ -30,35 +30,35 @@ import PassepartoutLibrary
|
||||||
struct ShortcutsView: View {
|
struct ShortcutsView: View {
|
||||||
enum ModalType: Identifiable {
|
enum ModalType: Identifiable {
|
||||||
case edit(shortcut: Shortcut)
|
case edit(shortcut: Shortcut)
|
||||||
|
|
||||||
case add(shortcut: INShortcut)
|
case add(shortcut: INShortcut)
|
||||||
|
|
||||||
// XXX: alert ids
|
// XXX: alert ids
|
||||||
var id: Int {
|
var id: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case .edit: return 1
|
case .edit: return 1
|
||||||
|
|
||||||
case .add: return 2
|
case .add: return 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StateObject private var intentsManager = IntentsManager()
|
@StateObject private var intentsManager = IntentsManager()
|
||||||
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
private let target: Profile
|
private let target: Profile
|
||||||
|
|
||||||
@State private var modalType: ModalType?
|
@State private var modalType: ModalType?
|
||||||
|
|
||||||
@State private var isNavigationPresented = false
|
@State private var isNavigationPresented = false
|
||||||
|
|
||||||
@State private var pendingShortcut: INShortcut?
|
@State private var pendingShortcut: INShortcut?
|
||||||
|
|
||||||
init(target: Profile) {
|
init(target: Profile) {
|
||||||
self.target = target
|
self.target = target
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
if !intentsManager.shortcuts.isEmpty {
|
if !intentsManager.shortcuts.isEmpty {
|
||||||
|
@ -69,16 +69,16 @@ struct ShortcutsView: View {
|
||||||
themeCloseItem(presentationMode: presentationMode)
|
themeCloseItem(presentationMode: presentationMode)
|
||||||
}.sheet(item: $modalType, content: presentedModal)
|
}.sheet(item: $modalType, content: presentedModal)
|
||||||
.themeAnimation(on: intentsManager.isReloadingShortcuts)
|
.themeAnimation(on: intentsManager.isReloadingShortcuts)
|
||||||
|
|
||||||
// IntentsUI
|
// IntentsUI
|
||||||
.onReceive(intentsManager.shouldDismissIntentView) { _ in
|
.onReceive(intentsManager.shouldDismissIntentView) { _ in
|
||||||
modalType = nil
|
modalType = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigationTitle(Unlocalized.Other.siri)
|
.navigationTitle(Unlocalized.Other.siri)
|
||||||
.themeSecondaryView()
|
.themeSecondaryView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var shortcutsSection: some View {
|
private var shortcutsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
ForEach(relevantShortcuts, content: rowView)
|
ForEach(relevantShortcuts, content: rowView)
|
||||||
|
@ -86,13 +86,13 @@ struct ShortcutsView: View {
|
||||||
Text(L10n.Shortcuts.Edit.Sections.All.header)
|
Text(L10n.Shortcuts.Edit.Sections.All.header)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var relevantShortcuts: [Shortcut] {
|
private var relevantShortcuts: [Shortcut] {
|
||||||
intentsManager.shortcuts.values.filter {
|
intentsManager.shortcuts.values.filter {
|
||||||
$0.isRelevant(to: target)
|
$0.isRelevant(to: target)
|
||||||
}.sorted()
|
}.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addSection: some View {
|
private var addSection: some View {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(isActive: $isNavigationPresented) {
|
NavigationLink(isActive: $isNavigationPresented) {
|
||||||
|
@ -116,7 +116,7 @@ struct ShortcutsView: View {
|
||||||
shortcut: shortcut,
|
shortcut: shortcut,
|
||||||
delegate: intentsManager
|
delegate: intentsManager
|
||||||
)
|
)
|
||||||
|
|
||||||
case .add(let shortcut):
|
case .add(let shortcut):
|
||||||
IntentAddView(
|
IntentAddView(
|
||||||
shortcut: shortcut,
|
shortcut: shortcut,
|
||||||
|
@ -124,7 +124,7 @@ struct ShortcutsView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rowView(forShortcut vs: Shortcut) -> some View {
|
private func rowView(forShortcut vs: Shortcut) -> some View {
|
||||||
Button {
|
Button {
|
||||||
presentEditShortcut(vs)
|
presentEditShortcut(vs)
|
||||||
|
@ -143,7 +143,7 @@ struct ShortcutsView: View {
|
||||||
presentAddShortcut(pendingShortcut)
|
presentAddShortcut(pendingShortcut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentEditShortcut(_ shortcut: Shortcut) {
|
private func presentEditShortcut(_ shortcut: Shortcut) {
|
||||||
modalType = .edit(shortcut: shortcut)
|
modalType = .edit(shortcut: shortcut)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,14 +28,14 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
struct VPNStatusText: View {
|
struct VPNStatusText: View {
|
||||||
@ObservedObject private var currentVPNState: ObservableVPNState
|
@ObservedObject private var currentVPNState: ObservableVPNState
|
||||||
|
|
||||||
let isActiveProfile: Bool
|
let isActiveProfile: Bool
|
||||||
|
|
||||||
init(isActiveProfile: Bool) {
|
init(isActiveProfile: Bool) {
|
||||||
currentVPNState = .shared
|
currentVPNState = .shared
|
||||||
self.isActiveProfile = isActiveProfile
|
self.isActiveProfile = isActiveProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(statusText)
|
Text(statusText)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,11 +36,11 @@ struct VPNToggle: View {
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
private let profile: Profile
|
private let profile: Profile
|
||||||
|
|
||||||
@Binding private var interactiveProfile: Profile?
|
@Binding private var interactiveProfile: Profile?
|
||||||
|
|
||||||
private let rateLimit: Int
|
private let rateLimit: Int
|
||||||
|
|
||||||
private var isEnabled: Binding<Bool> {
|
private var isEnabled: Binding<Bool> {
|
||||||
.init {
|
.init {
|
||||||
isActiveProfile && currentVPNState.isEnabled && !shouldPromptForAccount
|
isActiveProfile && currentVPNState.isEnabled && !shouldPromptForAccount
|
||||||
|
@ -56,11 +56,11 @@ struct VPNToggle: View {
|
||||||
enableVPN()
|
enableVPN()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isActiveProfile: Bool {
|
private var isActiveProfile: Bool {
|
||||||
profileManager.isActiveProfile(profile.id)
|
profileManager.isActiveProfile(profile.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var shouldPromptForAccount: Bool {
|
private var shouldPromptForAccount: Bool {
|
||||||
profile.account.authenticationMethod == .interactive && (currentVPNState.vpnStatus == .disconnecting || currentVPNState.vpnStatus == .disconnected)
|
profile.account.authenticationMethod == .interactive && (currentVPNState.vpnStatus == .disconnecting || currentVPNState.vpnStatus == .disconnected)
|
||||||
}
|
}
|
||||||
|
@ -68,9 +68,9 @@ struct VPNToggle: View {
|
||||||
private var isEligibleForSiri: Bool {
|
private var isEligibleForSiri: Bool {
|
||||||
productManager.isEligible(forFeature: .siriShortcuts)
|
productManager.isEligible(forFeature: .siriShortcuts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var canToggle = true
|
@State private var canToggle = true
|
||||||
|
|
||||||
init(profile: Profile, interactiveProfile: Binding<Profile?>, rateLimit: Int) {
|
init(profile: Profile, interactiveProfile: Binding<Profile?>, rateLimit: Int) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
vpnManager = .shared
|
vpnManager = .shared
|
||||||
|
@ -86,7 +86,7 @@ struct VPNToggle: View {
|
||||||
.disabled(!canToggle)
|
.disabled(!canToggle)
|
||||||
.themeAnimation(on: currentVPNState.isEnabled)
|
.themeAnimation(on: currentVPNState.isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enableVPN() {
|
private func enableVPN() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
canToggle = false
|
canToggle = false
|
||||||
|
@ -101,7 +101,7 @@ struct VPNToggle: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func disableVPN() {
|
private func disableVPN() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
canToggle = false
|
canToggle = false
|
||||||
|
@ -109,7 +109,7 @@ struct VPNToggle: View {
|
||||||
canToggle = true
|
canToggle = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func donateIntents(withProfile profile: Profile) {
|
private func donateIntents(withProfile profile: Profile) {
|
||||||
|
|
||||||
// eligibility: donate intents if eligible for Siri
|
// eligibility: donate intents if eligible for Siri
|
||||||
|
|
|
@ -52,7 +52,7 @@ extension View {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withLeadingLabel(_ text: String, color: Color? = nil, image: String) -> some View {
|
func withLeadingLabel(_ text: String, color: Color? = nil, image: String) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Label(text, image: image)
|
Label(text, image: image)
|
||||||
|
@ -80,7 +80,7 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withTrailingCheckmark(when condition: Bool) -> some View {
|
func withTrailingCheckmark(when condition: Bool) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
self
|
self
|
||||||
|
@ -91,7 +91,7 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withTrailingProgress(when condition: Bool) -> some View {
|
func withTrailingProgress(when condition: Bool) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
self
|
self
|
||||||
|
@ -145,7 +145,7 @@ private extension View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct HostingWindowFinder: UIViewRepresentable {
|
private struct HostingWindowFinder: UIViewRepresentable {
|
||||||
var callback: (UIWindow?) -> ()
|
var callback: (UIWindow?) -> Void
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIView(context: Context) -> UIView {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
|
|
|
@ -30,11 +30,11 @@ import PassepartoutLibrary
|
||||||
@MainActor
|
@MainActor
|
||||||
class CoreContext {
|
class CoreContext {
|
||||||
let store: KeyValueStore
|
let store: KeyValueStore
|
||||||
|
|
||||||
private let profilesPersistence: Persistence
|
private let profilesPersistence: Persistence
|
||||||
|
|
||||||
private let providersPersistence: Persistence
|
private let providersPersistence: Persistence
|
||||||
|
|
||||||
var urlsForProfiles: [URL]? {
|
var urlsForProfiles: [URL]? {
|
||||||
profilesPersistence.containerURLs
|
profilesPersistence.containerURLs
|
||||||
}
|
}
|
||||||
|
@ -44,18 +44,18 @@ class CoreContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
let upgradeManager: UpgradeManager
|
let upgradeManager: UpgradeManager
|
||||||
|
|
||||||
let providerManager: ProviderManager
|
let providerManager: ProviderManager
|
||||||
|
|
||||||
let profileManager: ProfileManager
|
let profileManager: ProfileManager
|
||||||
|
|
||||||
let vpnManager: VPNManager
|
let vpnManager: VPNManager
|
||||||
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
init(store: KeyValueStore) {
|
init(store: KeyValueStore) {
|
||||||
self.store = store
|
self.store = store
|
||||||
|
|
||||||
let persistenceManager = PersistenceManager(store: store)
|
let persistenceManager = PersistenceManager(store: store)
|
||||||
profilesPersistence = persistenceManager.profilesPersistence(
|
profilesPersistence = persistenceManager.profilesPersistence(
|
||||||
withName: Constants.Persistence.profilesContainerName
|
withName: Constants.Persistence.profilesContainerName
|
||||||
|
@ -106,12 +106,12 @@ class CoreContext {
|
||||||
providerManager: providerManager,
|
providerManager: providerManager,
|
||||||
strategy: strategy
|
strategy: strategy
|
||||||
)
|
)
|
||||||
|
|
||||||
// post
|
// post
|
||||||
|
|
||||||
configureObjects()
|
configureObjects()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureObjects() {
|
private func configureObjects() {
|
||||||
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
|
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
|
||||||
vpnManager.tunnelLogPath = Constants.Log.Tunnel.path
|
vpnManager.tunnelLogPath = Constants.Log.Tunnel.path
|
||||||
|
|
|
@ -41,16 +41,16 @@ extension PassepartoutError {
|
||||||
switch self {
|
switch self {
|
||||||
case .missingProfile:
|
case .missingProfile:
|
||||||
return V.missingProfile
|
return V.missingProfile
|
||||||
|
|
||||||
case .missingAccount:
|
case .missingAccount:
|
||||||
return V.missingAccount
|
return V.missingAccount
|
||||||
|
|
||||||
case .missingProviderServer:
|
case .missingProviderServer:
|
||||||
return V.missingProviderServer
|
return V.missingProviderServer
|
||||||
|
|
||||||
case .missingProviderPreset:
|
case .missingProviderPreset:
|
||||||
return V.missingProviderPreset
|
return V.missingProviderPreset
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ extension Network.Choice {
|
||||||
switch self {
|
switch self {
|
||||||
case .automatic:
|
case .automatic:
|
||||||
return L10n.Global.Strings.automatic
|
return L10n.Global.Strings.automatic
|
||||||
|
|
||||||
case .manual:
|
case .manual:
|
||||||
return L10n.Global.Strings.manual
|
return L10n.Global.Strings.manual
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ extension Network.DNSSettings.ConfigurationType {
|
||||||
switch self {
|
switch self {
|
||||||
case .plain:
|
case .plain:
|
||||||
return Unlocalized.DNS.plain
|
return Unlocalized.DNS.plain
|
||||||
|
|
||||||
case .https:
|
case .https:
|
||||||
return Unlocalized.Network.https
|
return Unlocalized.Network.https
|
||||||
|
|
||||||
|
@ -133,10 +133,10 @@ extension Network.ProxySettings.ConfigurationType {
|
||||||
switch self {
|
switch self {
|
||||||
case .manual:
|
case .manual:
|
||||||
return L10n.Global.Strings.manual
|
return L10n.Global.Strings.manual
|
||||||
|
|
||||||
case .pac:
|
case .pac:
|
||||||
return Unlocalized.Network.proxyAutoConfiguration
|
return Unlocalized.Network.proxyAutoConfiguration
|
||||||
|
|
||||||
case .disabled:
|
case .disabled:
|
||||||
return L10n.Global.Strings.disabled
|
return L10n.Global.Strings.disabled
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,10 +43,10 @@ extension OpenVPN.CompressionFraming {
|
||||||
switch self {
|
switch self {
|
||||||
case .disabled:
|
case .disabled:
|
||||||
return L10n.Global.Strings.disabled
|
return L10n.Global.Strings.disabled
|
||||||
|
|
||||||
case .compLZO:
|
case .compLZO:
|
||||||
return Unlocalized.OpenVPN.compLZO
|
return Unlocalized.OpenVPN.compLZO
|
||||||
|
|
||||||
case .compress, .compressV2:
|
case .compress, .compressV2:
|
||||||
return Unlocalized.OpenVPN.compress
|
return Unlocalized.OpenVPN.compress
|
||||||
}
|
}
|
||||||
|
@ -59,10 +59,10 @@ extension OpenVPN.CompressionAlgorithm {
|
||||||
switch self {
|
switch self {
|
||||||
case .disabled:
|
case .disabled:
|
||||||
return L10n.Global.Strings.disabled
|
return L10n.Global.Strings.disabled
|
||||||
|
|
||||||
case .LZO:
|
case .LZO:
|
||||||
return Unlocalized.OpenVPN.lzo
|
return Unlocalized.OpenVPN.lzo
|
||||||
|
|
||||||
case .other:
|
case .other:
|
||||||
return V.CompressionAlgorithm.Value.other
|
return V.CompressionAlgorithm.Value.other
|
||||||
}
|
}
|
||||||
|
@ -90,26 +90,26 @@ extension OpenVPN.XORMethod {
|
||||||
switch self {
|
switch self {
|
||||||
case .xormask:
|
case .xormask:
|
||||||
return Unlocalized.OpenVPN.XOR.xormask.rawValue
|
return Unlocalized.OpenVPN.XOR.xormask.rawValue
|
||||||
|
|
||||||
case .xorptrpos:
|
case .xorptrpos:
|
||||||
return Unlocalized.OpenVPN.XOR.xorptrpos.rawValue
|
return Unlocalized.OpenVPN.XOR.xorptrpos.rawValue
|
||||||
|
|
||||||
case .reverse:
|
case .reverse:
|
||||||
return Unlocalized.OpenVPN.XOR.reverse.rawValue
|
return Unlocalized.OpenVPN.XOR.reverse.rawValue
|
||||||
|
|
||||||
case .obfuscate:
|
case .obfuscate:
|
||||||
return Unlocalized.OpenVPN.XOR.obfuscate.rawValue
|
return Unlocalized.OpenVPN.XOR.obfuscate.rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var localizedLongDescription: String {
|
var localizedLongDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .xormask(let mask):
|
case .xormask(let mask):
|
||||||
return "\(localizedDescription) \(mask.toHex())"
|
return "\(localizedDescription) \(mask.toHex())"
|
||||||
|
|
||||||
case .obfuscate(let mask):
|
case .obfuscate(let mask):
|
||||||
return "\(localizedDescription) \(mask.toHex())"
|
return "\(localizedDescription) \(mask.toHex())"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return localizedDescription
|
return localizedDescription
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ extension Bool {
|
||||||
let V = L10n.Global.Strings.self
|
let V = L10n.Global.Strings.self
|
||||||
return self ? V.enabled : V.disabled
|
return self ? V.enabled : V.disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
var localizedDescriptionAsRandomizeHostnames: String {
|
var localizedDescriptionAsRandomizeHostnames: String {
|
||||||
let V = L10n.Global.Strings.self
|
let V = L10n.Global.Strings.self
|
||||||
return self ? V.enabled : V.disabled
|
return self ? V.enabled : V.disabled
|
||||||
|
@ -151,10 +151,10 @@ extension OpenVPN.PullMask {
|
||||||
switch self {
|
switch self {
|
||||||
case .routes:
|
case .routes:
|
||||||
return L10n.Endpoint.Advanced.Openvpn.Items.Route.caption
|
return L10n.Endpoint.Advanced.Openvpn.Items.Route.caption
|
||||||
|
|
||||||
case .dns:
|
case .dns:
|
||||||
return Unlocalized.Network.dns
|
return Unlocalized.Network.dns
|
||||||
|
|
||||||
case .proxy:
|
case .proxy:
|
||||||
return L10n.Global.Strings.proxy
|
return L10n.Global.Strings.proxy
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ extension ProviderManager {
|
||||||
}
|
}
|
||||||
return profile.providerPreset(server)?.localizedDescription
|
return profile.providerPreset(server)?.localizedDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
func localizedInfrastructureUpdate(forProfile profile: Profile) -> String? {
|
func localizedInfrastructureUpdate(forProfile profile: Profile) -> String? {
|
||||||
guard let providerName = profile.header.providerName else {
|
guard let providerName = profile.header.providerName else {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -35,13 +35,13 @@ extension VPNStatus {
|
||||||
switch self {
|
switch self {
|
||||||
case .connecting:
|
case .connecting:
|
||||||
return L10n.Tunnelkit.Vpn.connecting
|
return L10n.Tunnelkit.Vpn.connecting
|
||||||
|
|
||||||
case .connected:
|
case .connected:
|
||||||
return L10n.Tunnelkit.Vpn.active
|
return L10n.Tunnelkit.Vpn.active
|
||||||
|
|
||||||
case .disconnecting:
|
case .disconnecting:
|
||||||
return L10n.Tunnelkit.Vpn.disconnecting
|
return L10n.Tunnelkit.Vpn.disconnecting
|
||||||
|
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
return L10n.Tunnelkit.Vpn.inactive
|
return L10n.Tunnelkit.Vpn.inactive
|
||||||
}
|
}
|
||||||
|
@ -121,37 +121,37 @@ extension Error {
|
||||||
}
|
}
|
||||||
return localizedDescription
|
return localizedDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ovpnErrorDescription(_ error: OpenVPNProviderError) -> String? {
|
private func ovpnErrorDescription(_ error: OpenVPNProviderError) -> String? {
|
||||||
let V = L10n.Tunnelkit.Errors.Vpn.self
|
let V = L10n.Tunnelkit.Errors.Vpn.self
|
||||||
switch error {
|
switch error {
|
||||||
case .socketActivity, .timeout:
|
case .socketActivity, .timeout:
|
||||||
return V.timeout
|
return V.timeout
|
||||||
|
|
||||||
case .dnsFailure:
|
case .dnsFailure:
|
||||||
return V.dns
|
return V.dns
|
||||||
|
|
||||||
case .tlsInitialization, .tlsServerVerification, .tlsHandshake:
|
case .tlsInitialization, .tlsServerVerification, .tlsHandshake:
|
||||||
return V.tls
|
return V.tls
|
||||||
|
|
||||||
case .authentication:
|
case .authentication:
|
||||||
return V.auth
|
return V.auth
|
||||||
|
|
||||||
case .encryptionInitialization, .encryptionData:
|
case .encryptionInitialization, .encryptionData:
|
||||||
return V.encryption
|
return V.encryption
|
||||||
|
|
||||||
case .serverCompression, .lzo:
|
case .serverCompression, .lzo:
|
||||||
return V.compression
|
return V.compression
|
||||||
|
|
||||||
case .networkChanged:
|
case .networkChanged:
|
||||||
return V.network
|
return V.network
|
||||||
|
|
||||||
case .routing:
|
case .routing:
|
||||||
return V.routing
|
return V.routing
|
||||||
|
|
||||||
case .gatewayUnattainable:
|
case .gatewayUnattainable:
|
||||||
return V.gateway
|
return V.gateway
|
||||||
|
|
||||||
case .serverShutdown:
|
case .serverShutdown:
|
||||||
return V.shutdown
|
return V.shutdown
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ extension Error {
|
||||||
pp_log.error("Could not parse configuration URL: \(localizedDescription)")
|
pp_log.error("Could not parse configuration URL: \(localizedDescription)")
|
||||||
return L10n.Tunnelkit.Errors.parsing(localizedDescription)
|
return L10n.Tunnelkit.Errors.parsing(localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ovpnErrorDescription(_ error: OpenVPN.ConfigurationError) -> String {
|
private func ovpnErrorDescription(_ error: OpenVPN.ConfigurationError) -> String {
|
||||||
let V = L10n.Tunnelkit.Errors.Openvpn.self
|
let V = L10n.Tunnelkit.Errors.Openvpn.self
|
||||||
switch error {
|
switch error {
|
||||||
|
@ -203,7 +203,7 @@ extension Error {
|
||||||
case .missingConfiguration(let option):
|
case .missingConfiguration(let option):
|
||||||
pp_log.error("Could not parse configuration URL: missing configuration, \(option)")
|
pp_log.error("Could not parse configuration URL: missing configuration, \(option)")
|
||||||
return V.requiredOption(option)
|
return V.requiredOption(option)
|
||||||
|
|
||||||
case .unsupportedConfiguration(var option):
|
case .unsupportedConfiguration(var option):
|
||||||
if option.contains("external") {
|
if option.contains("external") {
|
||||||
option.append(" (see FAQ)")
|
option.append(" (see FAQ)")
|
||||||
|
|
|
@ -28,24 +28,24 @@ import PassepartoutLibrary
|
||||||
|
|
||||||
enum Unlocalized {
|
enum Unlocalized {
|
||||||
static let appName = Constants.Global.appName
|
static let appName = Constants.Global.appName
|
||||||
|
|
||||||
enum Placeholders {
|
enum Placeholders {
|
||||||
static let empty = ""
|
static let empty = ""
|
||||||
|
|
||||||
static let address = "0.0.0.0"
|
static let address = "0.0.0.0"
|
||||||
|
|
||||||
static let port = "8080"
|
static let port = "8080"
|
||||||
|
|
||||||
static let hostname = "example.com"
|
static let hostname = "example.com"
|
||||||
|
|
||||||
static let dohURL = "https://example.com/dns-query"
|
static let dohURL = "https://example.com/dns-query"
|
||||||
|
|
||||||
static let dotServerName = hostname
|
static let dotServerName = hostname
|
||||||
|
|
||||||
static let dnsAddress = address
|
static let dnsAddress = address
|
||||||
|
|
||||||
static let dnsDomain = hostname
|
static let dnsDomain = hostname
|
||||||
|
|
||||||
static let pacURL = "https://proxy/auto-conf"
|
static let pacURL = "https://proxy/auto-conf"
|
||||||
|
|
||||||
static let proxyBypassDomain = hostname
|
static let proxyBypassDomain = hostname
|
||||||
|
@ -54,24 +54,24 @@ enum Unlocalized {
|
||||||
enum DNS {
|
enum DNS {
|
||||||
static let plain = "Cleartext"
|
static let plain = "Cleartext"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Keychain {
|
enum Keychain {
|
||||||
static func passwordLabel(_ profileName: String, vpnProtocol: VPNProtocolType) -> String {
|
static func passwordLabel(_ profileName: String, vpnProtocol: VPNProtocolType) -> String {
|
||||||
"\(Constants.Global.appName): \(profileName) (\(vpnProtocol.description))"
|
"\(Constants.Global.appName): \(profileName) (\(vpnProtocol.description))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Issues {
|
enum Issues {
|
||||||
static let recipient = "issues@\(Constants.Domain.name)"
|
static let recipient = "issues@\(Constants.Domain.name)"
|
||||||
|
|
||||||
static let subject = "\(appName) - Report issue"
|
static let subject = "\(appName) - Report issue"
|
||||||
|
|
||||||
static func body(_ description: String, _ metadata: String) -> String {
|
static func body(_ description: String, _ metadata: String) -> String {
|
||||||
"Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
|
"Hi,\n\n\(description)\n\n\(metadata)\n\nRegards"
|
||||||
}
|
}
|
||||||
|
|
||||||
static let template = "description of the issue: "
|
static let template = "description of the issue: "
|
||||||
|
|
||||||
static let maxLogBytes = UInt64(20000)
|
static let maxLogBytes = UInt64(20000)
|
||||||
|
|
||||||
enum Filenames {
|
enum Filenames {
|
||||||
|
@ -81,13 +81,13 @@ enum Unlocalized {
|
||||||
let iso = fmt.string(from: Date())
|
let iso = fmt.string(from: Date())
|
||||||
return "debug-\(iso).txt"
|
return "debug-\(iso).txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
static let configuration = "profile.ovpn"
|
static let configuration = "profile.ovpn"
|
||||||
// static let configuration = "profile.ovpn.txt"
|
// static let configuration = "profile.ovpn.txt"
|
||||||
|
|
||||||
static let template = "description of the issue: "
|
static let template = "description of the issue: "
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MIME {
|
enum MIME {
|
||||||
static let debugLog = "text/plain"
|
static let debugLog = "text/plain"
|
||||||
|
|
||||||
|
@ -98,9 +98,9 @@ enum Unlocalized {
|
||||||
|
|
||||||
enum Social {
|
enum Social {
|
||||||
static let reddit = "Reddit"
|
static let reddit = "Reddit"
|
||||||
|
|
||||||
private static let twitterHashtags = ["OpenVPN", "WireGuard", "iOS", "macOS"]
|
private static let twitterHashtags = ["OpenVPN", "WireGuard", "iOS", "macOS"]
|
||||||
|
|
||||||
static func twitterIntent(withMessage message: String) -> URL {
|
static func twitterIntent(withMessage message: String) -> URL {
|
||||||
var text = message
|
var text = message
|
||||||
for ht in twitterHashtags {
|
for ht in twitterHashtags {
|
||||||
|
@ -121,7 +121,7 @@ enum Unlocalized {
|
||||||
static let recipient = "translate@\(Constants.Domain.name)"
|
static let recipient = "translate@\(Constants.Domain.name)"
|
||||||
|
|
||||||
static let subject = "\(appName) - Translations"
|
static let subject = "\(appName) - Translations"
|
||||||
|
|
||||||
static func body(_ description: String) -> String {
|
static func body(_ description: String) -> String {
|
||||||
"Hi,\n\n\(description)\n\nRegards"
|
"Hi,\n\n\(description)\n\nRegards"
|
||||||
}
|
}
|
||||||
|
@ -148,9 +148,9 @@ enum Unlocalized {
|
||||||
|
|
||||||
enum Credits {
|
enum Credits {
|
||||||
typealias License = (String, String, URL)
|
typealias License = (String, String, URL)
|
||||||
|
|
||||||
typealias Notice = (String, String)
|
typealias Notice = (String, String)
|
||||||
|
|
||||||
static let author = "Davide De Rosa"
|
static let author = "Davide De Rosa"
|
||||||
|
|
||||||
static let licenses: [License] = [(
|
static let licenses: [License] = [(
|
||||||
|
@ -178,7 +178,7 @@ enum Unlocalized {
|
||||||
"MIT",
|
"MIT",
|
||||||
URL(string: "https://raw.githubusercontent.com/SwiftyBeaver/SwiftyBeaver/master/LICENSE")!
|
URL(string: "https://raw.githubusercontent.com/SwiftyBeaver/SwiftyBeaver/master/LICENSE")!
|
||||||
)]
|
)]
|
||||||
|
|
||||||
static let notices: [Notice] = [(
|
static let notices: [Notice] = [(
|
||||||
"Circle Icons",
|
"Circle Icons",
|
||||||
"The logo is taken from the awesome Circle Icons set by Nick Roach."
|
"The logo is taken from the awesome Circle Icons set by Nick Roach."
|
||||||
|
@ -196,63 +196,63 @@ enum Unlocalized {
|
||||||
|
|
||||||
enum About {
|
enum About {
|
||||||
static let github = "GitHub"
|
static let github = "GitHub"
|
||||||
|
|
||||||
static let readme = "README"
|
static let readme = "README"
|
||||||
|
|
||||||
static let changelog = "CHANGELOG"
|
static let changelog = "CHANGELOG"
|
||||||
|
|
||||||
static let faq = "FAQ"
|
static let faq = "FAQ"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum VPN {
|
enum VPN {
|
||||||
static let vpn = "VPN"
|
static let vpn = "VPN"
|
||||||
|
|
||||||
static let certificateAuthority = "CA"
|
static let certificateAuthority = "CA"
|
||||||
|
|
||||||
static let xor = "XOR"
|
static let xor = "XOR"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OpenVPN {
|
enum OpenVPN {
|
||||||
static let compLZO = "--comp-lzo"
|
static let compLZO = "--comp-lzo"
|
||||||
|
|
||||||
static let compress = "--compress"
|
static let compress = "--compress"
|
||||||
|
|
||||||
static let lzo = "LZO"
|
static let lzo = "LZO"
|
||||||
|
|
||||||
enum XOR: String {
|
enum XOR: String {
|
||||||
case xormask
|
case xormask
|
||||||
|
|
||||||
case xorptrpos
|
case xorptrpos
|
||||||
|
|
||||||
case reverse
|
case reverse
|
||||||
|
|
||||||
case obfuscate
|
case obfuscate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Network {
|
enum Network {
|
||||||
static let dns = "DNS"
|
static let dns = "DNS"
|
||||||
|
|
||||||
static let tls = "TLS"
|
static let tls = "TLS"
|
||||||
|
|
||||||
static let https = "HTTPS"
|
static let https = "HTTPS"
|
||||||
|
|
||||||
static let url = "URL"
|
static let url = "URL"
|
||||||
|
|
||||||
static let mtu = "MTU"
|
static let mtu = "MTU"
|
||||||
|
|
||||||
static let ipv4 = "IPv4"
|
static let ipv4 = "IPv4"
|
||||||
|
|
||||||
static let ipv6 = "IPv6"
|
static let ipv6 = "IPv6"
|
||||||
|
|
||||||
static let ssid = "SSID"
|
static let ssid = "SSID"
|
||||||
|
|
||||||
static let proxyAutoConfiguration = "PAC"
|
static let proxyAutoConfiguration = "PAC"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Other {
|
enum Other {
|
||||||
static let siri = "Siri"
|
static let siri = "Siri"
|
||||||
|
|
||||||
static let totp = "TOTP"
|
static let totp = "TOTP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,8 @@ import Foundation
|
||||||
@objc(MacBridge)
|
@objc(MacBridge)
|
||||||
public protocol MacBridge: NSObjectProtocol {
|
public protocol MacBridge: NSObjectProtocol {
|
||||||
init()
|
init()
|
||||||
|
|
||||||
var utils: MacUtils { get }
|
var utils: MacUtils { get }
|
||||||
|
|
||||||
var menu: MacMenu { get }
|
var menu: MacMenu { get }
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import Foundation
|
||||||
@objc
|
@objc
|
||||||
public protocol MacUtils {
|
public protocol MacUtils {
|
||||||
var isStartedByLauncher: Bool { get }
|
var isStartedByLauncher: Bool { get }
|
||||||
|
|
||||||
func sendAppToBackground()
|
func sendAppToBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,13 @@ import Foundation
|
||||||
@objc(LightProfile)
|
@objc(LightProfile)
|
||||||
public protocol LightProfile {
|
public protocol LightProfile {
|
||||||
var id: UUID { get }
|
var id: UUID { get }
|
||||||
|
|
||||||
var name: String { get }
|
var name: String { get }
|
||||||
|
|
||||||
var vpnProtocol: String { get }
|
var vpnProtocol: String { get }
|
||||||
|
|
||||||
var isActive: Bool { get }
|
var isActive: Bool { get }
|
||||||
|
|
||||||
var providerName: String? { get }
|
var providerName: String? { get }
|
||||||
|
|
||||||
var providerServer: LightProviderServer? { get }
|
var providerServer: LightProviderServer? { get }
|
||||||
|
@ -50,13 +50,13 @@ extension LightProfile {
|
||||||
@objc
|
@objc
|
||||||
public protocol LightProfileManager {
|
public protocol LightProfileManager {
|
||||||
var hasProfiles: Bool { get }
|
var hasProfiles: Bool { get }
|
||||||
|
|
||||||
var profiles: [LightProfile] { get }
|
var profiles: [LightProfile] { get }
|
||||||
|
|
||||||
var activeProfileId: UUID? { get }
|
var activeProfileId: UUID? { get }
|
||||||
|
|
||||||
var activeProfileName: String? { get }
|
var activeProfileName: String? { get }
|
||||||
|
|
||||||
var delegate: LightProfileManagerDelegate? { get set }
|
var delegate: LightProfileManagerDelegate? { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,20 +39,20 @@ public protocol LightProviderLocation {
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
|
|
||||||
var countryCode: String { get }
|
var countryCode: String { get }
|
||||||
|
|
||||||
var servers: [LightProviderServer] { get }
|
var servers: [LightProviderServer] { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(LightProviderServer)
|
@objc(LightProviderServer)
|
||||||
public protocol LightProviderServer {
|
public protocol LightProviderServer {
|
||||||
var description: String { get }
|
var description: String { get }
|
||||||
|
|
||||||
var longDescription: String { get }
|
var longDescription: String { get }
|
||||||
|
|
||||||
var categoryName: String { get }
|
var categoryName: String { get }
|
||||||
|
|
||||||
var locationId: String { get }
|
var locationId: String { get }
|
||||||
|
|
||||||
var serverId: String { get }
|
var serverId: String { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ public protocol LightProviderServer {
|
||||||
@objc
|
@objc
|
||||||
public protocol LightProviderManager {
|
public protocol LightProviderManager {
|
||||||
var delegate: LightProviderManagerDelegate? { get set }
|
var delegate: LightProviderManagerDelegate? { get set }
|
||||||
|
|
||||||
func categories(_ name: String, vpnProtocol: String) -> [LightProviderCategory]
|
func categories(_ name: String, vpnProtocol: String) -> [LightProviderCategory]
|
||||||
|
|
||||||
func downloadIfNeeded(_ name: String, vpnProtocol: String)
|
func downloadIfNeeded(_ name: String, vpnProtocol: String)
|
||||||
|
|
|
@ -28,6 +28,6 @@ import Foundation
|
||||||
@objc
|
@objc
|
||||||
public protocol LightUtils {
|
public protocol LightUtils {
|
||||||
var launchesOnLogin: Bool { get set }
|
var launchesOnLogin: Bool { get set }
|
||||||
|
|
||||||
func requestScene()
|
func requestScene()
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,11 +28,11 @@ import Foundation
|
||||||
@objc(LightVPNStatus)
|
@objc(LightVPNStatus)
|
||||||
public enum LightVPNStatus: Int {
|
public enum LightVPNStatus: Int {
|
||||||
case connecting
|
case connecting
|
||||||
|
|
||||||
case connected
|
case connected
|
||||||
|
|
||||||
case disconnecting
|
case disconnecting
|
||||||
|
|
||||||
case disconnected
|
case disconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,19 +42,19 @@ public protocol LightVPNManager {
|
||||||
var isEnabled: Bool { get }
|
var isEnabled: Bool { get }
|
||||||
|
|
||||||
var vpnStatus: LightVPNStatus { get }
|
var vpnStatus: LightVPNStatus { get }
|
||||||
|
|
||||||
func connect(with profileId: UUID)
|
func connect(with profileId: UUID)
|
||||||
|
|
||||||
func connect(with profileId: UUID, to serverId: String)
|
func connect(with profileId: UUID, to serverId: String)
|
||||||
|
|
||||||
func disconnect()
|
func disconnect()
|
||||||
|
|
||||||
func toggle()
|
func toggle()
|
||||||
|
|
||||||
func reconnect()
|
func reconnect()
|
||||||
|
|
||||||
func addDelegate(_ delegate: LightVPNManagerDelegate, withIdentifier identifier: String)
|
func addDelegate(_ delegate: LightVPNManagerDelegate, withIdentifier identifier: String)
|
||||||
|
|
||||||
func removeDelegate(withIdentifier identifier: String)
|
func removeDelegate(withIdentifier identifier: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,10 +28,10 @@ import Foundation
|
||||||
@objc(MenuBuilder)
|
@objc(MenuBuilder)
|
||||||
public protocol MenuBuilder: NSObjectProtocol {
|
public protocol MenuBuilder: NSObjectProtocol {
|
||||||
weak var delegate: MenuDelegate? { get set }
|
weak var delegate: MenuDelegate? { get set }
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
func sendAppToBackground()
|
func sendAppToBackground()
|
||||||
|
|
||||||
func buildMenu()
|
func buildMenu()
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import AppKit
|
||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||||
private let appURL = Constants.Launcher.appURL
|
private let appURL = Constants.Launcher.appURL
|
||||||
|
|
||||||
private var isAppRunning: Bool {
|
private var isAppRunning: Bool {
|
||||||
NSWorkspace.shared.runningApplications.contains {
|
NSWorkspace.shared.runningApplications.contains {
|
||||||
$0.bundleIdentifier == Constants.Launcher.appId
|
$0.bundleIdentifier == Constants.Launcher.appId
|
||||||
|
@ -40,12 +40,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||||
NSApp.terminate(self)
|
NSApp.terminate(self)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let cfg = NSWorkspace.OpenConfiguration()
|
let cfg = NSWorkspace.OpenConfiguration()
|
||||||
cfg.hides = true
|
cfg.hides = true
|
||||||
cfg.activates = false
|
cfg.activates = false
|
||||||
cfg.addsToRecentItems = false
|
cfg.addsToRecentItems = false
|
||||||
NSWorkspace.shared.openApplication(at: appURL, configuration: cfg) { app, error in
|
NSWorkspace.shared.openApplication(at: appURL, configuration: cfg) { _, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
NSLog("Unable to launch main app: \(error)")
|
NSLog("Unable to launch main app: \(error)")
|
||||||
return
|
return
|
||||||
|
|
|
@ -30,7 +30,7 @@ extension Constants {
|
||||||
static var bundle: Bundle {
|
static var bundle: Bundle {
|
||||||
Bundle(for: PassepartoutMac.self)
|
Bundle(for: PassepartoutMac.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
static let appLauncherId: String = bundleConfig("launcher_id", in: bundle)
|
static let appLauncherId: String = bundleConfig("launcher_id", in: bundle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ extension LightVPNStatus {
|
||||||
switch self {
|
switch self {
|
||||||
case .connected, .disconnected:
|
case .connected, .disconnected:
|
||||||
resourceName = "StatusActive"
|
resourceName = "StatusActive"
|
||||||
|
|
||||||
case .connecting, .disconnecting:
|
case .connecting, .disconnecting:
|
||||||
resourceName = "StatusPending"
|
resourceName = "StatusPending"
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,12 @@ extension LightVPNStatus {
|
||||||
}
|
}
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageAlpha: Double {
|
var imageAlpha: Double {
|
||||||
switch self {
|
switch self {
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
return 0.5
|
return 0.5
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 1.0
|
return 1.0
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,14 +27,14 @@ import Foundation
|
||||||
|
|
||||||
class DefaultMacMenu: MacMenu {
|
class DefaultMacMenu: MacMenu {
|
||||||
weak var delegate: MacMenuDelegate?
|
weak var delegate: MacMenuDelegate?
|
||||||
|
|
||||||
private lazy var menu: PassepartoutMenu = {
|
private lazy var menu: PassepartoutMenu = {
|
||||||
guard let delegate = delegate else {
|
guard let delegate = delegate else {
|
||||||
fatalError("Must set MacMenu.delegate")
|
fatalError("Must set MacMenu.delegate")
|
||||||
}
|
}
|
||||||
return PassepartoutMenu(macMenuDelegate: delegate)
|
return PassepartoutMenu(macMenuDelegate: delegate)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func install() {
|
func install() {
|
||||||
menu.install()
|
menu.install()
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue