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