mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-02-17 05:12:18 +00:00
Create basic UI for TV app (#798)
Start with the profile tab. Left to do: search and settings. Fixes and refactoring: - Listen to changes in current profile in ExtendedTunnel - Externalize style from TunnelToggleButton and ConnectionStatusText (renamed from View) - Add ThemeCountryText for convenience
This commit is contained in:
parent
357c505cc0
commit
72e784272a
@ -19,6 +19,7 @@
|
|||||||
0EC066D12C7DC47600D88A94 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */; platformFilter = ios; };
|
0EC066D12C7DC47600D88A94 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */; platformFilter = ios; };
|
||||||
0EC332CA2B8A1808000B9C2F /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EC332C92B8A1808000B9C2F /* NetworkExtension.framework */; };
|
0EC332CA2B8A1808000B9C2F /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EC332C92B8A1808000B9C2F /* NetworkExtension.framework */; };
|
||||||
0EC332D22B8A1808000B9C2F /* PassepartoutTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0EC332C82B8A1808000B9C2F /* PassepartoutTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
0EC332D22B8A1808000B9C2F /* PassepartoutTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0EC332C82B8A1808000B9C2F /* PassepartoutTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
0EC4A7132CD597EF00B7CAAD /* AppUIPlatform in Frameworks */ = {isa = PBXBuildFile; productRef = 0EC4A7122CD597EF00B7CAAD /* AppUIPlatform */; };
|
||||||
0EC797422B9378E000C093B7 /* Shared+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797402B9378E000C093B7 /* Shared+App.swift */; };
|
0EC797422B9378E000C093B7 /* Shared+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797402B9378E000C093B7 /* Shared+App.swift */; };
|
||||||
0EC797432B9378E000C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; };
|
0EC797432B9378E000C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; };
|
||||||
0EC797442B93790600C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; };
|
0EC797442B93790600C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; };
|
||||||
@ -27,8 +28,6 @@
|
|||||||
0EDE56EA2CABE40D0082D21C /* Intents.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0EDE56E62CABE40D0082D21C /* Intents.plist */; };
|
0EDE56EA2CABE40D0082D21C /* Intents.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0EDE56E62CABE40D0082D21C /* Intents.plist */; };
|
||||||
0EDE56FA2CABE42E0082D21C /* PassepartoutIntents.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 0EDE56F02CABE42E0082D21C /* PassepartoutIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
0EDE56FA2CABE42E0082D21C /* PassepartoutIntents.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 0EDE56F02CABE42E0082D21C /* PassepartoutIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
0EDE57002CABE4B50082D21C /* IntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE56E72CABE40D0082D21C /* IntentsExtension.swift */; };
|
0EDE57002CABE4B50082D21C /* IntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE56E72CABE40D0082D21C /* IntentsExtension.swift */; };
|
||||||
0EE8D7DD2CD1107E00F6600C /* AppUIMain in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, macos, ); productRef = 0EE8D7DC2CD1107E00F6600C /* AppUIMain */; };
|
|
||||||
0EE8D7DF2CD1108900F6600C /* AppUITV in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, ); productRef = 0EE8D7DE2CD1108900F6600C /* AppUITV */; };
|
|
||||||
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE8D7E02CD112C200F6600C /* App+tvOS.swift */; };
|
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE8D7E02CD112C200F6600C /* App+tvOS.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@ -145,8 +144,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
0EE8D7DF2CD1108900F6600C /* AppUITV in Frameworks */,
|
0EC4A7132CD597EF00B7CAAD /* AppUIPlatform in Frameworks */,
|
||||||
0EE8D7DD2CD1107E00F6600C /* AppUIMain in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -292,8 +290,7 @@
|
|||||||
);
|
);
|
||||||
name = Passepartout;
|
name = Passepartout;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
0EE8D7DC2CD1107E00F6600C /* AppUIMain */,
|
0EC4A7122CD597EF00B7CAAD /* AppUIPlatform */,
|
||||||
0EE8D7DE2CD1108900F6600C /* AppUITV */,
|
|
||||||
);
|
);
|
||||||
productName = PassepartoutKit;
|
productName = PassepartoutKit;
|
||||||
productReference = 0E06D18F2B87629100176E1D /* Passepartout.app */;
|
productReference = 0E06D18F2B87629100176E1D /* Passepartout.app */;
|
||||||
@ -998,13 +995,9 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = TunnelLibrary;
|
productName = TunnelLibrary;
|
||||||
};
|
};
|
||||||
0EE8D7DC2CD1107E00F6600C /* AppUIMain */ = {
|
0EC4A7122CD597EF00B7CAAD /* AppUIPlatform */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = AppUIMain;
|
productName = AppUIPlatform;
|
||||||
};
|
|
||||||
0EE8D7DE2CD1108900F6600C /* AppUITV */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = AppUITV;
|
|
||||||
};
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,10 @@ let package = Package(
|
|||||||
name: "AppUIMain",
|
name: "AppUIMain",
|
||||||
targets: ["AppUIMain"]
|
targets: ["AppUIMain"]
|
||||||
),
|
),
|
||||||
|
.library(
|
||||||
|
name: "AppUIPlatform",
|
||||||
|
targets: ["AppUIPlatform"]
|
||||||
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "AppUITV",
|
name: "AppUITV",
|
||||||
targets: ["AppUITV"]
|
targets: ["AppUITV"]
|
||||||
@ -109,6 +113,13 @@ let package = Package(
|
|||||||
.process("Resources")
|
.process("Resources")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "AppUIPlatform",
|
||||||
|
dependencies: [
|
||||||
|
.target(name: "AppUIMain", condition: .when(platforms: [.iOS, .macOS])),
|
||||||
|
.target(name: "AppUITV", condition: .when(platforms: [.tvOS]))
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "AppUITV",
|
name: "AppUITV",
|
||||||
dependencies: ["AppUI"]
|
dependencies: ["AppUI"]
|
||||||
|
@ -84,6 +84,14 @@ public final class ExtendedTunnel: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
|
tunnel
|
||||||
|
.$currentProfile
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.objectWillChange.send()
|
||||||
|
}
|
||||||
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
Timer
|
Timer
|
||||||
.publish(every: interval, on: .main, in: .common)
|
.publish(every: interval, on: .main, in: .common)
|
||||||
.autoconnect()
|
.autoconnect()
|
||||||
|
@ -297,6 +297,8 @@ public enum Strings {
|
|||||||
public static let routing = Strings.tr("Localizable", "global.routing", fallback: "Routing")
|
public static let routing = Strings.tr("Localizable", "global.routing", fallback: "Routing")
|
||||||
/// Save
|
/// Save
|
||||||
public static let save = Strings.tr("Localizable", "global.save", fallback: "Save")
|
public static let save = Strings.tr("Localizable", "global.save", fallback: "Save")
|
||||||
|
/// Select
|
||||||
|
public static let select = Strings.tr("Localizable", "global.select", fallback: "Select")
|
||||||
/// Server
|
/// Server
|
||||||
public static let server = Strings.tr("Localizable", "global.server", fallback: "Server")
|
public static let server = Strings.tr("Localizable", "global.server", fallback: "Server")
|
||||||
/// Servers
|
/// Servers
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
"global.routes" = "Routes";
|
"global.routes" = "Routes";
|
||||||
"global.routing" = "Routing";
|
"global.routing" = "Routing";
|
||||||
"global.save" = "Save";
|
"global.save" = "Save";
|
||||||
|
"global.select" = "Select";
|
||||||
"global.server" = "Server";
|
"global.server" = "Server";
|
||||||
"global.servers" = "Servers";
|
"global.servers" = "Servers";
|
||||||
"global.settings" = "Settings";
|
"global.settings" = "Settings";
|
||||||
|
@ -148,13 +148,13 @@ extension ThemeTappableText {
|
|||||||
|
|
||||||
extension ThemeTextField {
|
extension ThemeTextField {
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
commonView
|
labeledView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ThemeSecureField {
|
extension ThemeSecureField {
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
commonView
|
labeledView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,15 +108,13 @@ extension ThemeTappableText {
|
|||||||
|
|
||||||
extension ThemeTextField {
|
extension ThemeTextField {
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
commonView
|
fieldView
|
||||||
.labelsHidden()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ThemeSecureField {
|
extension ThemeSecureField {
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
commonView
|
fieldView
|
||||||
.labelsHidden()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,13 +67,13 @@ extension ThemeSectionWithHeaderFooterModifier {
|
|||||||
|
|
||||||
extension ThemeTextField {
|
extension ThemeTextField {
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
commonView
|
TextField(placeholder, text: $text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ThemeSecureField {
|
extension ThemeSecureField {
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
commonView
|
SecureField(placeholder, text: $text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ extension Theme {
|
|||||||
case profilesGrid
|
case profilesGrid
|
||||||
case profilesList
|
case profilesList
|
||||||
case remove
|
case remove
|
||||||
|
case search
|
||||||
case settings
|
case settings
|
||||||
case share
|
case share
|
||||||
case show
|
case show
|
||||||
@ -90,6 +91,7 @@ extension Theme.ImageName {
|
|||||||
case .profilesGrid: return "square.grid.2x2"
|
case .profilesGrid: return "square.grid.2x2"
|
||||||
case .profilesList: return "rectangle.grid.1x2"
|
case .profilesList: return "rectangle.grid.1x2"
|
||||||
case .remove: return "minus"
|
case .remove: return "minus"
|
||||||
|
case .search: return "magnifyingglass"
|
||||||
case .settings: return "gearshape"
|
case .settings: return "gearshape"
|
||||||
case .share: return "square.and.arrow.up"
|
case .share: return "square.and.arrow.up"
|
||||||
case .show: return "eye"
|
case .show: return "eye"
|
||||||
|
@ -91,6 +91,25 @@ public struct ThemeImageLabel: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct ThemeCountryText: View {
|
||||||
|
private let code: String
|
||||||
|
|
||||||
|
private let title: String?
|
||||||
|
|
||||||
|
public init(_ code: String, title: String? = nil) {
|
||||||
|
self.code = code
|
||||||
|
self.title = title ?? code.localizedAsRegionCode
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Text(
|
||||||
|
[code, title]
|
||||||
|
.compactMap { $0 }
|
||||||
|
.joined(separator: " ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ThemeCountryFlag: View {
|
public struct ThemeCountryFlag: View {
|
||||||
private let code: String?
|
private let code: String?
|
||||||
|
|
||||||
@ -124,12 +143,12 @@ public struct ThemeCountryFlag: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct ThemeTextField: View {
|
public struct ThemeTextField: View {
|
||||||
private let title: String?
|
let title: String?
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
private var text: String
|
var text: String
|
||||||
|
|
||||||
private let placeholder: String
|
let placeholder: String
|
||||||
|
|
||||||
public init(_ title: String, text: Binding<String>, placeholder: String) {
|
public init(_ title: String, text: Binding<String>, placeholder: String) {
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -138,7 +157,7 @@ public struct ThemeTextField: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var commonView: some View {
|
var labeledView: some View {
|
||||||
if let title {
|
if let title {
|
||||||
LabeledContent {
|
LabeledContent {
|
||||||
fieldView
|
fieldView
|
||||||
@ -150,18 +169,18 @@ public struct ThemeTextField: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fieldView: some View {
|
var fieldView: some View {
|
||||||
TextField(title ?? "", text: $text, prompt: Text(placeholder))
|
TextField(title ?? "", text: $text, prompt: Text(placeholder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ThemeSecureField: View {
|
public struct ThemeSecureField: View {
|
||||||
private let title: String?
|
let title: String?
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
private var text: String
|
var text: String
|
||||||
|
|
||||||
private let placeholder: String
|
let placeholder: String
|
||||||
|
|
||||||
public init(title: String?, text: Binding<String>, placeholder: String) {
|
public init(title: String?, text: Binding<String>, placeholder: String) {
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -170,7 +189,7 @@ public struct ThemeSecureField: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var commonView: some View {
|
var labeledView: some View {
|
||||||
if let title {
|
if let title {
|
||||||
LabeledContent {
|
LabeledContent {
|
||||||
fieldView
|
fieldView
|
||||||
@ -182,7 +201,7 @@ public struct ThemeSecureField: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fieldView: some View {
|
var fieldView: some View {
|
||||||
RevealingSecureField(title ?? "", text: $text, prompt: Text(placeholder), imageWidth: 30.0) {
|
RevealingSecureField(title ?? "", text: $text, prompt: Text(placeholder), imageWidth: 30.0) {
|
||||||
ThemeImage(.hide)
|
ThemeImage(.hide)
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// ConnectionStatusView.swift
|
// ConnectionStatusText.swift
|
||||||
// Passepartout
|
// Passepartout
|
||||||
//
|
//
|
||||||
// Created by Davide De Rosa on 9/4/24.
|
// Created by Davide De Rosa on 9/4/24.
|
||||||
@ -28,7 +28,7 @@ import Foundation
|
|||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct ConnectionStatusView: View, ThemeProviding {
|
public struct ConnectionStatusText: View, ThemeProviding {
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
public var theme: Theme
|
public var theme: Theme
|
||||||
@ -42,12 +42,10 @@ public struct ConnectionStatusView: View, ThemeProviding {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Text(statusDescription)
|
Text(statusDescription)
|
||||||
.font(.headline)
|
|
||||||
.foregroundStyle(tunnel.statusColor(theme))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ConnectionStatusView {
|
private extension ConnectionStatusText {
|
||||||
var statusDescription: String {
|
var statusDescription: String {
|
||||||
if let lastErrorCode = tunnel.lastErrorCode {
|
if let lastErrorCode = tunnel.lastErrorCode {
|
||||||
return lastErrorCode.localizedDescription
|
return lastErrorCode.localizedDescription
|
||||||
@ -76,7 +74,7 @@ private extension ConnectionStatusView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Connected") {
|
#Preview("Connected") {
|
||||||
ConnectionStatusView(tunnel: .mock)
|
ConnectionStatusText(tunnel: .mock)
|
||||||
.task {
|
.task {
|
||||||
try? await ExtendedTunnel.mock.connect(with: .mock, processor: .mock)
|
try? await ExtendedTunnel.mock.connect(with: .mock, processor: .mock)
|
||||||
}
|
}
|
||||||
@ -95,7 +93,7 @@ private extension ConnectionStatusView {
|
|||||||
} catch {
|
} catch {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
return ConnectionStatusView(tunnel: .mock)
|
return ConnectionStatusText(tunnel: .mock)
|
||||||
.task {
|
.task {
|
||||||
try? await ExtendedTunnel.mock.connect(with: profile, processor: .mock)
|
try? await ExtendedTunnel.mock.connect(with: profile, processor: .mock)
|
||||||
}
|
}
|
@ -29,11 +29,6 @@ import SwiftUI
|
|||||||
import UtilsLibrary
|
import UtilsLibrary
|
||||||
|
|
||||||
public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View {
|
public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View {
|
||||||
public enum Style {
|
|
||||||
case plain
|
|
||||||
|
|
||||||
case color
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
public var theme: Theme
|
public var theme: Theme
|
||||||
@ -44,8 +39,6 @@ public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View
|
|||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var profileProcessor: ProfileProcessor
|
private var profileProcessor: ProfileProcessor
|
||||||
|
|
||||||
private let style: Style
|
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
private var tunnel: ExtendedTunnel
|
private var tunnel: ExtendedTunnel
|
||||||
|
|
||||||
@ -63,7 +56,6 @@ public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View
|
|||||||
private let label: (Bool) -> Label
|
private let label: (Bool) -> Label
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
style: Style = .plain,
|
|
||||||
tunnel: ExtendedTunnel,
|
tunnel: ExtendedTunnel,
|
||||||
profile: Profile?,
|
profile: Profile?,
|
||||||
nextProfileId: Binding<Profile.ID?>,
|
nextProfileId: Binding<Profile.ID?>,
|
||||||
@ -72,7 +64,6 @@ public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View
|
|||||||
onProviderEntityRequired: ((Profile) -> Void)? = nil,
|
onProviderEntityRequired: ((Profile) -> Void)? = nil,
|
||||||
label: @escaping (Bool) -> Label
|
label: @escaping (Bool) -> Label
|
||||||
) {
|
) {
|
||||||
self.style = style
|
|
||||||
self.tunnel = tunnel
|
self.tunnel = tunnel
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
_nextProfileId = nextProfileId
|
_nextProfileId = nextProfileId
|
||||||
@ -86,7 +77,6 @@ public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View
|
|||||||
Button(action: tryPerform) {
|
Button(action: tryPerform) {
|
||||||
label(canConnect)
|
label(canConnect)
|
||||||
}
|
}
|
||||||
.foregroundStyle(color)
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.cursor(.hand)
|
.cursor(.hand)
|
||||||
@ -103,16 +93,6 @@ private extension TunnelToggleButton {
|
|||||||
var canConnect: Bool {
|
var canConnect: Bool {
|
||||||
!isInstalled || (tunnel.status == .inactive && tunnel.currentProfile?.onDemand != true)
|
!isInstalled || (tunnel.status == .inactive && tunnel.currentProfile?.onDemand != true)
|
||||||
}
|
}
|
||||||
|
|
||||||
var color: Color {
|
|
||||||
switch style {
|
|
||||||
case .plain:
|
|
||||||
return .primary
|
|
||||||
|
|
||||||
case .color:
|
|
||||||
return tunnel.statusColor(theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension TunnelToggleButton {
|
private extension TunnelToggleButton {
|
||||||
|
@ -100,14 +100,15 @@ private extension InstalledProfileView {
|
|||||||
var statusView: some View {
|
var statusView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
providerSelectorButton
|
providerSelectorButton
|
||||||
ConnectionStatusView(tunnel: tunnel)
|
ConnectionStatusText(tunnel: tunnel)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundStyle(tunnel.statusColor(theme))
|
||||||
.opacity(installedOpacity)
|
.opacity(installedOpacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var toggleButton: some View {
|
var toggleButton: some View {
|
||||||
TunnelToggleButton(
|
TunnelToggleButton(
|
||||||
style: .color,
|
|
||||||
tunnel: tunnel,
|
tunnel: tunnel,
|
||||||
profile: profile,
|
profile: profile,
|
||||||
nextProfileId: $nextProfileId,
|
nextProfileId: $nextProfileId,
|
||||||
@ -121,6 +122,7 @@ private extension InstalledProfileView {
|
|||||||
)
|
)
|
||||||
// TODO: #584, necessary to avoid cell selection
|
// TODO: #584, necessary to avoid cell selection
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.foregroundStyle(tunnel.statusColor(theme))
|
||||||
.opacity(installedOpacity)
|
.opacity(installedOpacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ private extension ProfileRowView {
|
|||||||
.contentShape(.rect)
|
.contentShape(.rect)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sharingView: some View {
|
var sharingView: some View {
|
||||||
|
@ -81,7 +81,7 @@ private extension VPNFiltersView {
|
|||||||
Text(Strings.Global.any)
|
Text(Strings.Global.any)
|
||||||
.tag(nil as String?)
|
.tag(nil as String?)
|
||||||
ForEach(model.countries, id: \.code) {
|
ForEach(model.countries, id: \.code) {
|
||||||
Text("\($0.code.asCountryCodeEmoji) \($0.description)")
|
ThemeCountryText($0.code, title: $0.description)
|
||||||
.tag($0.code as String?)
|
.tag($0.code as String?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,7 @@ private extension VPNProviderServerView.ServersSubview {
|
|||||||
DisclosureGroup {
|
DisclosureGroup {
|
||||||
ForEach(servers, id: \.id, content: serverView)
|
ForEach(servers, id: \.id, content: serverView)
|
||||||
} label: {
|
} label: {
|
||||||
Text("\(code.asCountryCodeEmoji) \(code.localizedAsRegionCode ?? code)")
|
ThemeCountryText(code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ extension VPNProviderServerView {
|
|||||||
.width(10.0)
|
.width(10.0)
|
||||||
|
|
||||||
TableColumn(Strings.Global.region) { server in
|
TableColumn(Strings.Global.region) { server in
|
||||||
Text("\(server.provider.countryCode.asCountryCodeEmoji) \(server.region)")
|
ThemeCountryText(server.provider.countryCode, title: server.region)
|
||||||
.help(server.region)
|
.help(server.region)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
Passepartout/Library/Sources/AppUIPlatform/Dummy.swift
Normal file
26
Passepartout/Library/Sources/AppUIPlatform/Dummy.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// Dummy.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 11/2/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
@ -27,8 +27,6 @@ import AppLibrary
|
|||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// FIXME: #788, UI for Apple TV
|
|
||||||
|
|
||||||
public struct AppCoordinator: View, AppCoordinatorConforming {
|
public struct AppCoordinator: View, AppCoordinatorConforming {
|
||||||
private let profileManager: ProfileManager
|
private let profileManager: ProfileManager
|
||||||
|
|
||||||
@ -43,18 +41,42 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ProfilesView(profileManager: profileManager)
|
debugChanges()
|
||||||
|
return TabView {
|
||||||
|
profileView
|
||||||
|
.tabItem {
|
||||||
|
Text(Strings.Global.profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchView
|
||||||
|
.tabItem {
|
||||||
|
ThemeImage(.search)
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsView
|
||||||
|
.tabItem {
|
||||||
|
ThemeImage(.settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProfilesView: View {
|
private extension AppCoordinator {
|
||||||
|
var profileView: some View {
|
||||||
|
ProfileView(profileManager: profileManager, tunnel: tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
@ObservedObject
|
// FIXME: #788, UI for TV
|
||||||
var profileManager: ProfileManager
|
var searchView: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Search")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
// FIXME: #788, UI for TV
|
||||||
ForEach(profileManager.headers, id: \.id) {
|
var settingsView: some View {
|
||||||
Text($0.name)
|
VStack {
|
||||||
|
Text("Settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// ActiveProfileView.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 11/1/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
|
import PassepartoutKit
|
||||||
|
import SwiftUI
|
||||||
|
import UtilsLibrary
|
||||||
|
|
||||||
|
struct ActiveProfileView: View {
|
||||||
|
let profile: Profile?
|
||||||
|
|
||||||
|
let firstProfileId: Profile.ID?
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var tunnel: ExtendedTunnel
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var isSwitching: Bool
|
||||||
|
|
||||||
|
@FocusState.Binding
|
||||||
|
var focusedField: ProfileView.Field?
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var interactiveManager: InteractiveManager
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var errorHandler: ErrorHandler
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
VStack {
|
||||||
|
currentProfileView
|
||||||
|
statusView
|
||||||
|
Group {
|
||||||
|
toggleConnectionButton
|
||||||
|
switchProfileButton
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 100)
|
||||||
|
}
|
||||||
|
.padding(.top, 100)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ActiveProfileView {
|
||||||
|
var currentProfileView: some View {
|
||||||
|
Text(profile?.name ?? Strings.Views.Profiles.Rows.notInstalled)
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusView: some View {
|
||||||
|
ConnectionStatusText(tunnel: tunnel)
|
||||||
|
.font(.title2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var toggleConnectionButton: some View {
|
||||||
|
TunnelToggleButton(
|
||||||
|
tunnel: tunnel,
|
||||||
|
profile: profile,
|
||||||
|
nextProfileId: .constant(nil),
|
||||||
|
interactiveManager: interactiveManager,
|
||||||
|
errorHandler: errorHandler,
|
||||||
|
label: {
|
||||||
|
Text($0 ? Strings.Global.connect : Strings.Global.disconnect)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.focused($focusedField, equals: .connect)
|
||||||
|
}
|
||||||
|
|
||||||
|
var switchProfileButton: some View {
|
||||||
|
Button {
|
||||||
|
if let focus = tunnel.currentProfile?.id ?? firstProfileId {
|
||||||
|
focusedField = .profile(focus)
|
||||||
|
}
|
||||||
|
isSwitching.toggle()
|
||||||
|
} label: {
|
||||||
|
Text(Strings.Global.select)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.focused($focusedField, equals: .switchProfile)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// ProfileListView.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 11/1/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
|
import PassepartoutKit
|
||||||
|
import SwiftUI
|
||||||
|
import UtilsLibrary
|
||||||
|
|
||||||
|
struct ProfileListView: View {
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var profileManager: ProfileManager
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var tunnel: ExtendedTunnel
|
||||||
|
|
||||||
|
@FocusState.Binding
|
||||||
|
var focusedField: ProfileView.Field?
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var interactiveManager: InteractiveManager
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var errorHandler: ErrorHandler
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
ForEach(profileManager.headers, id: \.id, content: toggleButton(for:))
|
||||||
|
} header: {
|
||||||
|
Text(Strings.Views.Profiles.Folders.default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.grouped)
|
||||||
|
.scrollClipDisabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ProfileListView {
|
||||||
|
func toggleButton(for header: ProfileHeader) -> some View {
|
||||||
|
TunnelToggleButton(
|
||||||
|
tunnel: tunnel,
|
||||||
|
profile: profileManager.profile(withId: header.id),
|
||||||
|
nextProfileId: .constant(nil),
|
||||||
|
interactiveManager: interactiveManager,
|
||||||
|
errorHandler: errorHandler,
|
||||||
|
label: { _ in
|
||||||
|
toggleView(for: header)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleView(for header: ProfileHeader) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(header.name)
|
||||||
|
Spacer()
|
||||||
|
if header.id == tunnel.currentProfile?.id {
|
||||||
|
ThemeImage(.marked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.headline)
|
||||||
|
.focused($focusedField, equals: .profile(header.id))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
//
|
||||||
|
// ProfileView.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 10/31/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppLibrary
|
||||||
|
import AppUI
|
||||||
|
import PassepartoutKit
|
||||||
|
import SwiftUI
|
||||||
|
import UtilsLibrary
|
||||||
|
|
||||||
|
// FIXME: #788, UI for TV
|
||||||
|
|
||||||
|
struct ProfileView: View, TunnelInstallationProviding {
|
||||||
|
enum Field: Hashable {
|
||||||
|
case connect
|
||||||
|
|
||||||
|
case switchProfile
|
||||||
|
|
||||||
|
case profile(Profile.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var theme: Theme
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var profileManager: ProfileManager
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var tunnel: ExtendedTunnel
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isSwitching = false
|
||||||
|
|
||||||
|
@FocusState
|
||||||
|
private var focusedField: Field?
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var interactiveManager = InteractiveManager()
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var errorHandler: ErrorHandler = .default()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geo in
|
||||||
|
HStack(spacing: .zero) {
|
||||||
|
VStack {
|
||||||
|
activeView
|
||||||
|
.padding(.horizontal)
|
||||||
|
.frame(width: geo.size.width * 0.5)
|
||||||
|
.focusSection()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
if isSwitching {
|
||||||
|
listView
|
||||||
|
.padding(.horizontal)
|
||||||
|
.frame(width: geo.size.width * 0.5)
|
||||||
|
.focusSection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(edges: .horizontal)
|
||||||
|
.background(theme.primaryColor.gradient)
|
||||||
|
.animation(.default, value: isSwitching)
|
||||||
|
.withErrorHandler(errorHandler)
|
||||||
|
.themeModal(isPresented: $interactiveManager.isPresented) {
|
||||||
|
InteractiveView(manager: interactiveManager) {
|
||||||
|
errorHandler.handle(
|
||||||
|
$0,
|
||||||
|
title: Strings.Global.connection,
|
||||||
|
message: Strings.Views.Profiles.Errors.tunnel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onLoad {
|
||||||
|
focusedField = .switchProfile
|
||||||
|
}
|
||||||
|
.onChange(of: tunnel.status) { _, new in
|
||||||
|
if new == .activating {
|
||||||
|
isSwitching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: tunnel.currentProfile) { _, new in
|
||||||
|
if focusedField == .connect && new == nil {
|
||||||
|
focusedField = .switchProfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: focusedField) { _, new in
|
||||||
|
switch new {
|
||||||
|
case .connect:
|
||||||
|
isSwitching = false
|
||||||
|
|
||||||
|
case .switchProfile:
|
||||||
|
isSwitching = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ProfileView {
|
||||||
|
var currentProfile: Profile? {
|
||||||
|
guard let id = tunnel.currentProfile?.id else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return profileManager.profile(withId: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeView: some View {
|
||||||
|
ActiveProfileView(
|
||||||
|
profile: currentProfile,
|
||||||
|
firstProfileId: profileManager.headers.first?.id,
|
||||||
|
tunnel: tunnel,
|
||||||
|
isSwitching: $isSwitching,
|
||||||
|
focusedField: $focusedField,
|
||||||
|
interactiveManager: interactiveManager,
|
||||||
|
errorHandler: errorHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var listView: some View {
|
||||||
|
ProfileListView(
|
||||||
|
profileManager: profileManager,
|
||||||
|
tunnel: tunnel,
|
||||||
|
focusedField: $focusedField,
|
||||||
|
interactiveManager: interactiveManager,
|
||||||
|
errorHandler: errorHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ProfileView(
|
||||||
|
profileManager: .mock,
|
||||||
|
tunnel: .mock
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user