feat: Sync tab, hamburger menu, network status, browse UX
- Replace History tab with Sync tab (file list, NAS status, free-up-space retention) - Add AppMenuView hamburger sheet (History, Errors, Connection Status, Sync Activity) - BackupView: Kisani logo chip in toolbar, hamburger menu button, swipeable ring pages (progress/pending/failed/uploaded), always-visible compact stats strip - SettingsView: NETWORK section (SSID, NAS reachability, trusted-network warning, refresh button) - BrowseView: pull-to-refresh, long-press context menu on items - LANMonitor: nasReachable, nasCheckTime, checkNASReachability(host:port:) - ConnectionStore: localRetentionDays (0/60/90/365 days) - Regenerate xcodeproj with xcodegen to include new files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ struct SettingsView: View {
|
||||
VStack(spacing: AppTheme.sectionGap) {
|
||||
appearanceSection
|
||||
LANTriggerSection()
|
||||
networkStatusSection
|
||||
mediaSection
|
||||
quietHoursSection
|
||||
connectivitySection
|
||||
@@ -155,6 +156,144 @@ struct SettingsView: View {
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
|
||||
// MARK: — Network Status
|
||||
|
||||
@State private var isCheckingNetwork = false
|
||||
|
||||
private var networkStatusSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
sectionLabel("NETWORK")
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Wi-Fi / SSID row
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "wifi")
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(lanMonitor.currentSSID ?? (lanMonitor.isOnCellular ? "Cellular" : "No Wi-Fi"))
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.ink)
|
||||
Text(lanMonitor.isOnNetwork ? "Connected" : "Not connected")
|
||||
.font(AppTheme.caption())
|
||||
.foregroundStyle(AppTheme.inkTertiary)
|
||||
}
|
||||
Spacer()
|
||||
if lanMonitor.currentSSID != nil {
|
||||
let trusted = store.trustedSSIDs.contains(lanMonitor.currentSSID ?? "")
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(trusted ? AppTheme.positive : AppTheme.inkQuaternary)
|
||||
.frame(width: 5, height: 5)
|
||||
Text(trusted ? "Trusted" : "Unknown")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(trusted ? AppTheme.positive : AppTheme.inkTertiary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 13)
|
||||
|
||||
hairline
|
||||
|
||||
// NAS reachability row
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "externaldrive.connected.to.line.below")
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("NAS reachability")
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.ink)
|
||||
if let checkTime = lanMonitor.nasCheckTime {
|
||||
Text("Checked \(checkTime, format: .dateTime.hour().minute())")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(AppTheme.inkTertiary)
|
||||
} else {
|
||||
Text("Not yet checked")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(AppTheme.inkTertiary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Group {
|
||||
if let reachable = lanMonitor.nasReachable {
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(reachable ? AppTheme.positive : AppTheme.destructive)
|
||||
.frame(width: 5, height: 5)
|
||||
Text(reachable ? "Reachable" : "Offline")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(reachable ? AppTheme.positive : AppTheme.destructive)
|
||||
}
|
||||
} else {
|
||||
Text("—")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(AppTheme.inkQuaternary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 13)
|
||||
|
||||
hairline
|
||||
|
||||
// Warn if not on trusted network but NAS expected
|
||||
if let ssid = lanMonitor.currentSSID,
|
||||
!store.trustedSSIDs.isEmpty,
|
||||
!store.trustedSSIDs.contains(ssid) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 11, weight: .regular))
|
||||
.foregroundStyle(AppTheme.destructive.opacity(0.8))
|
||||
Text("Not on your preferred network — backup may not reach the NAS.")
|
||||
.font(AppTheme.micro())
|
||||
.foregroundStyle(AppTheme.destructive.opacity(0.7))
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
hairline
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
Button {
|
||||
guard let conn = store.savedConnection else { return }
|
||||
isCheckingNetwork = true
|
||||
Task {
|
||||
await lanMonitor.checkSSID()
|
||||
await lanMonitor.checkNASReachability(host: conn.host, port: conn.port)
|
||||
isCheckingNetwork = false
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
if isCheckingNetwork {
|
||||
ProgressView().scaleEffect(0.7).tint(AppTheme.inkSecondary)
|
||||
} else {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
}
|
||||
Text(isCheckingNetwork ? "Checking…" : "Refresh network status")
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.ink)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 13)
|
||||
}
|
||||
.buttonStyle(ScaleButtonStyle())
|
||||
.disabled(isCheckingNetwork)
|
||||
}
|
||||
.background(AppTheme.surfaceRaised)
|
||||
.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.nasReachable != nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Connectivity
|
||||
|
||||
private var connectivitySection: some View {
|
||||
|
||||
Reference in New Issue
Block a user