Simplify iOS network add flow
This commit is contained in:
parent
35f3b3ce4e
commit
36a54628ba
2 changed files with 181 additions and 42 deletions
|
|
@ -18,33 +18,43 @@ public struct BurrowView: View {
|
||||||
Text("Burrow")
|
Text("Burrow")
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
Text("Networks and accounts")
|
if showsHeaderSubtitle {
|
||||||
.font(.headline)
|
Text("Networks and accounts")
|
||||||
.foregroundStyle(.secondary)
|
.font(.headline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
if showsToolbarAddMenu {
|
||||||
Menu {
|
Spacer()
|
||||||
Button("Add WireGuard Network") {
|
Menu {
|
||||||
activeSheet = .wireGuard
|
Button("Add WireGuard Network") {
|
||||||
|
activeSheet = .wireGuard
|
||||||
|
}
|
||||||
|
Button("Save Tor Account") {
|
||||||
|
activeSheet = .tor
|
||||||
|
}
|
||||||
|
Button("Add Tailnet Account") {
|
||||||
|
activeSheet = .tailnet
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.title)
|
||||||
|
.accessibilityLabel("Add")
|
||||||
}
|
}
|
||||||
Button("Save Tor Account") {
|
|
||||||
activeSheet = .tor
|
|
||||||
}
|
|
||||||
Button("Add Tailnet Account") {
|
|
||||||
activeSheet = .tailnet
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "plus.circle.fill")
|
|
||||||
.font(.title)
|
|
||||||
.accessibilityLabel("Add")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
|
|
||||||
|
if showsInlineQuickActions {
|
||||||
|
quickAddSection
|
||||||
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
sectionHeader(
|
sectionHeader(
|
||||||
title: "Networks",
|
title: "Networks",
|
||||||
detail: "Stored daemon networks and their active account selectors"
|
detail: showsInlineQuickActions
|
||||||
|
? nil
|
||||||
|
: "Stored daemon networks and their active account selectors"
|
||||||
)
|
)
|
||||||
if let connectionError = networkViewModel.connectionError {
|
if let connectionError = networkViewModel.connectionError {
|
||||||
Text(connectionError)
|
Text(connectionError)
|
||||||
|
|
@ -54,25 +64,29 @@ public struct BurrowView: View {
|
||||||
NetworkCarouselView(networks: networkViewModel.cards)
|
NetworkCarouselView(networks: networkViewModel.cards)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
if showsAccountsSection {
|
||||||
sectionHeader(
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
title: "Accounts",
|
sectionHeader(
|
||||||
detail: "Per-network identities and sign-in state"
|
title: "Accounts",
|
||||||
)
|
detail: showsInlineQuickActions
|
||||||
if accountStore.accounts.isEmpty {
|
? nil
|
||||||
ContentUnavailableView(
|
: "Per-network identities and sign-in state"
|
||||||
"No Accounts Yet",
|
|
||||||
systemImage: "person.crop.circle.badge.plus",
|
|
||||||
description: Text("Save a Tor account or sign in to a Tailnet provider to keep network identities ready on this device.")
|
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity, minHeight: 180)
|
if accountStore.accounts.isEmpty {
|
||||||
} else {
|
ContentUnavailableView(
|
||||||
LazyVStack(spacing: 12) {
|
"No Accounts Yet",
|
||||||
ForEach(accountStore.accounts) { account in
|
systemImage: "person.crop.circle.badge.plus",
|
||||||
AccountRowView(
|
description: Text("Save a Tor account or sign in to a Tailnet provider to keep network identities ready on this device.")
|
||||||
account: account,
|
)
|
||||||
hasSecret: accountStore.hasStoredSecret(for: account)
|
.frame(maxWidth: .infinity, minHeight: 180)
|
||||||
)
|
} else {
|
||||||
|
LazyVStack(spacing: 12) {
|
||||||
|
ForEach(accountStore.accounts) { account in
|
||||||
|
AccountRowView(
|
||||||
|
account: account,
|
||||||
|
hasSecret: accountStore.hasStoredSecret(for: account)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +95,7 @@ public struct BurrowView: View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
sectionHeader(
|
sectionHeader(
|
||||||
title: "Tunnel",
|
title: "Tunnel",
|
||||||
detail: "Current system extension state"
|
detail: showsInlineQuickActions ? nil : "Current system extension state"
|
||||||
)
|
)
|
||||||
TunnelStatusView()
|
TunnelStatusView()
|
||||||
TunnelButton()
|
TunnelButton()
|
||||||
|
|
@ -120,18 +134,58 @@ public struct BurrowView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func sectionHeader(title: String, detail: String) -> some View {
|
private var quickAddSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
sectionHeader(title: "Add", detail: nil)
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
ForEach(ConfigurationSheet.allCases) { sheet in
|
||||||
|
QuickAddButton(sheet: sheet) {
|
||||||
|
activeSheet = sheet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func sectionHeader(title: String, detail: String?) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.title2.weight(.semibold))
|
.font(.title2.weight(.semibold))
|
||||||
Text(detail)
|
if let detail, !detail.isEmpty {
|
||||||
.font(.subheadline)
|
Text(detail)
|
||||||
.foregroundStyle(.secondary)
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var showsInlineQuickActions: Bool {
|
||||||
|
#if os(iOS)
|
||||||
|
true
|
||||||
|
#else
|
||||||
|
false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showsToolbarAddMenu: Bool {
|
||||||
|
!showsInlineQuickActions
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showsHeaderSubtitle: Bool {
|
||||||
|
!showsInlineQuickActions
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showsAccountsSection: Bool {
|
||||||
|
#if os(iOS)
|
||||||
|
!accountStore.accounts.isEmpty
|
||||||
|
#else
|
||||||
|
true
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ConfigurationSheet: String, Identifiable {
|
private enum ConfigurationSheet: String, CaseIterable, Identifiable {
|
||||||
case wireGuard
|
case wireGuard
|
||||||
case tor
|
case tor
|
||||||
case tailnet
|
case tailnet
|
||||||
|
|
@ -145,6 +199,75 @@ private enum ConfigurationSheet: String, Identifiable {
|
||||||
case .tailnet: .headscale
|
case .tailnet: .headscale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var iconName: String {
|
||||||
|
switch self {
|
||||||
|
case .wireGuard:
|
||||||
|
"wave.3.right"
|
||||||
|
case .tor:
|
||||||
|
"shield.lefthalf.filled.badge.checkmark"
|
||||||
|
case .tailnet:
|
||||||
|
"network.badge.shield.half.filled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quickActionTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .wireGuard:
|
||||||
|
"WireGuard"
|
||||||
|
case .tor:
|
||||||
|
"Tor"
|
||||||
|
case .tailnet:
|
||||||
|
"Tailnet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quickActionSubtitle: String {
|
||||||
|
switch self {
|
||||||
|
case .wireGuard:
|
||||||
|
"Import a tunnel"
|
||||||
|
case .tor:
|
||||||
|
"Save an Arti profile"
|
||||||
|
case .tailnet:
|
||||||
|
"Sign in or save a control plane"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quickActionColor: Color {
|
||||||
|
switch self {
|
||||||
|
case .wireGuard:
|
||||||
|
.blue
|
||||||
|
case .tor, .tailnet:
|
||||||
|
kind.accentColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct QuickAddButton: View {
|
||||||
|
let sheet: ConfigurationSheet
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
HStack(spacing: 14) {
|
||||||
|
Image(systemName: sheet.iconName)
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.frame(width: 24)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(sheet.quickActionTitle)
|
||||||
|
.font(.headline)
|
||||||
|
Text(sheet.quickActionSubtitle)
|
||||||
|
.font(.caption)
|
||||||
|
.opacity(0.88)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
|
||||||
|
}
|
||||||
|
.buttonStyle(.floating(color: sheet.quickActionColor, cornerRadius: 18))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AccountDraft {
|
private struct AccountDraft {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,28 @@ struct NetworkCarouselView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if networks.isEmpty {
|
if networks.isEmpty {
|
||||||
|
#if os(iOS)
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("No stored networks yet")
|
||||||
|
.font(.headline)
|
||||||
|
Text("WireGuard and Tailnet networks show up here as soon as you add one.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding()
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 18)
|
||||||
|
.fill(.thinMaterial)
|
||||||
|
)
|
||||||
|
#else
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
"No Networks Yet",
|
"No Networks Yet",
|
||||||
systemImage: "network.slash",
|
systemImage: "network.slash",
|
||||||
description: Text("Add a WireGuard network, or save a Tailnet account so Burrow can store a managed network when the daemon is reachable.")
|
description: Text("Add a WireGuard network, or save a Tailnet account so Burrow can store a managed network when the daemon is reachable.")
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity, minHeight: 175)
|
.frame(maxWidth: .infinity, minHeight: 175)
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
LazyHStack {
|
LazyHStack {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue