Add cellular data toggle, Tailscale remote access, fix ToggleRow padding
- ToggleRow: fix cramped layout — proper horizontal + vertical card padding - ConnectionStore: add allowCellularBackup, useTailscaleWhenRemote, tailscaleHost - LANMonitor: add isOnCellular (NWPath cellular interface), isTailscaleActive (detects utun interface with 100.x.x.x CGNAT address), openTailscaleApp() - BackupEngine: gate backup on cellular setting; resolveHost() switches to tailscaleHost when not on trusted LAN and Tailscale tunnel is active - SettingsView: CONNECTIVITY section (cellular toggle + live status), REMOTE ACCESS section (Tailscale toggle, host field, status dot, Open button) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var store: ConnectionStore
|
||||
@EnvironmentObject var lanMonitor: LANMonitor
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -12,6 +13,8 @@ struct SettingsView: View {
|
||||
LANTriggerSection()
|
||||
mediaSection
|
||||
quietHoursSection
|
||||
connectivitySection
|
||||
remoteAccessSection
|
||||
notificationSection
|
||||
}
|
||||
.padding(.horizontal, AppTheme.hPad)
|
||||
@@ -100,6 +103,123 @@ struct SettingsView: View {
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
|
||||
// MARK: — Connectivity
|
||||
|
||||
private var connectivitySection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
sectionLabel("CONNECTIVITY")
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ToggleRow(
|
||||
icon: "antenna.radiowaves.left.and.right",
|
||||
title: "Allow over cellular",
|
||||
subtitle: "Uses mobile data when Wi-Fi is unavailable",
|
||||
isOn: $store.allowCellularBackup
|
||||
)
|
||||
|
||||
// Live status chip
|
||||
if lanMonitor.isOnCellular {
|
||||
hairline
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(store.allowCellularBackup ? AppTheme.positive : AppTheme.destructive)
|
||||
.frame(width: 6, height: 6)
|
||||
Text(store.allowCellularBackup
|
||||
? "On cellular — backup allowed"
|
||||
: "On cellular — backup blocked")
|
||||
.font(AppTheme.micro(11))
|
||||
.foregroundStyle(store.allowCellularBackup
|
||||
? AppTheme.positive : AppTheme.destructive)
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
}
|
||||
.background(Color.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
||||
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: lanMonitor.isOnCellular)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Remote Access (Tailscale)
|
||||
|
||||
private var remoteAccessSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
sectionLabel("REMOTE ACCESS")
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ToggleRow(
|
||||
icon: "network.badge.shield.half.filled",
|
||||
title: "Use Tailscale when remote",
|
||||
subtitle: "Connects via VPN when not on home network",
|
||||
isOn: $store.useTailscaleWhenRemote
|
||||
)
|
||||
|
||||
if store.useTailscaleWhenRemote {
|
||||
hairline
|
||||
|
||||
// Tailscale host input
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "server.rack")
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Tailscale NAS host")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(AppTheme.inkTertiary)
|
||||
TextField("100.x.x.x or nas.tail-xxxxx.ts.net",
|
||||
text: $store.tailscaleHost)
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.ink)
|
||||
.keyboardType(.URL)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
hairline
|
||||
|
||||
// Tailscale status + open button
|
||||
HStack(spacing: 10) {
|
||||
Circle()
|
||||
.fill(lanMonitor.isTailscaleActive ? AppTheme.positive : AppTheme.inkQuaternary)
|
||||
.frame(width: 7, height: 7)
|
||||
Text(lanMonitor.isTailscaleActive ? "Tailscale connected" : "Tailscale not active")
|
||||
.font(AppTheme.caption())
|
||||
.foregroundStyle(lanMonitor.isTailscaleActive
|
||||
? AppTheme.positive : AppTheme.inkTertiary)
|
||||
Spacer()
|
||||
if !lanMonitor.isTailscaleActive {
|
||||
Button {
|
||||
lanMonitor.openTailscaleApp()
|
||||
} label: {
|
||||
Text("Open Tailscale")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(AppTheme.ink)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(AppTheme.surfaceSunken)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
|
||||
}
|
||||
.buttonStyle(ScaleButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
.background(Color.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
||||
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: store.useTailscaleWhenRemote)
|
||||
.animation(.spring(response: 0.25, dampingFraction: 0.8), value: lanMonitor.isTailscaleActive)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Notifications
|
||||
|
||||
private var notificationSection: some View {
|
||||
|
||||
Reference in New Issue
Block a user