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>
2026-05-16 17:58:00 +03:00
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
struct AppMenuView: View {
|
|
|
|
|
@EnvironmentObject var store: ConnectionStore
|
|
|
|
|
@EnvironmentObject var lanMonitor: LANMonitor
|
2026-05-17 12:12:26 +03:00
|
|
|
@EnvironmentObject var engine: BackupEngine
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
@State private var showClearSheet = false
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
@State private var selectedErrorEntry: BackupHistoryEntry?
|
|
|
|
|
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
private var errorEntries: [BackupHistoryEntry] {
|
|
|
|
|
store.historyEntries.filter { $0.failedCount > 0 }
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
private var lastSuccessfulEntry: BackupHistoryEntry? {
|
|
|
|
|
store.historyEntries.first { $0.failedCount == 0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: — Wi-Fi helpers
|
|
|
|
|
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
private var wifiSubtitle: String {
|
|
|
|
|
if lanMonitor.isOnCellular { return "Cellular only" }
|
|
|
|
|
if lanMonitor.isOnNetwork { return lanMonitor.currentSSID ?? "Connected" }
|
|
|
|
|
return "Not connected"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var wifiBadgeLabel: String {
|
|
|
|
|
if lanMonitor.isOnCellular { return "Cellular" }
|
|
|
|
|
if lanMonitor.isOnNetwork { return "Connected" }
|
|
|
|
|
return "Offline"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var wifiBadgeColor: Color {
|
2026-05-17 12:12:26 +03:00
|
|
|
if lanMonitor.isOnCellular { return .orange }
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
if lanMonitor.isOnNetwork { return AppTheme.positive }
|
2026-05-17 12:12:26 +03:00
|
|
|
return .orange
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
// MARK: — NAS helpers
|
|
|
|
|
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
private var nasBadgeLabel: String {
|
|
|
|
|
switch lanMonitor.nasReachable {
|
2026-05-17 12:01:42 +03:00
|
|
|
case true: return "Connected"
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
case false: return "Offline"
|
|
|
|
|
default: return "Unknown"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var nasBadgeColor: Color {
|
|
|
|
|
switch lanMonitor.nasReachable {
|
2026-05-17 12:01:42 +03:00
|
|
|
case true: return AppTheme.positive
|
2026-05-17 12:12:26 +03:00
|
|
|
case false: return .orange
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
default: return AppTheme.inkQuaternary
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
// MARK: — Session helpers
|
|
|
|
|
|
|
|
|
|
private var sessionColor: Color {
|
|
|
|
|
switch engine.job.status {
|
|
|
|
|
case .running: return AppTheme.interactive
|
|
|
|
|
case .paused: return .orange
|
|
|
|
|
default: return .orange
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var sessionTitle: String {
|
|
|
|
|
switch engine.job.status {
|
|
|
|
|
case .preparing: return "Scanning library…"
|
|
|
|
|
case .running: return "Uploading"
|
|
|
|
|
case .paused: return "Paused"
|
|
|
|
|
default: return "Active"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var sessionBadgeLabel: String {
|
|
|
|
|
switch engine.job.status {
|
|
|
|
|
case .preparing: return "Scanning"
|
|
|
|
|
case .running: return "Uploading"
|
|
|
|
|
case .paused: return "Paused"
|
|
|
|
|
default: return "Active"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: — Body
|
|
|
|
|
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
ZStack {
|
|
|
|
|
AppTheme.background.ignoresSafeArea()
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
VStack(spacing: AppTheme.sectionGap) {
|
|
|
|
|
connectionStatusSection
|
2026-05-17 12:12:26 +03:00
|
|
|
|
|
|
|
|
if engine.job.status.isActive {
|
|
|
|
|
currentSessionSection
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
if !errorEntries.isEmpty {
|
|
|
|
|
errorsSection
|
|
|
|
|
}
|
2026-05-17 12:12:26 +03:00
|
|
|
|
|
|
|
|
lastBackupSection
|
|
|
|
|
syncActivitySection
|
|
|
|
|
historySection
|
fix(signing): remove Access Wi-Fi entitlement (unsupported on free teams)
- Drop com.apple.developer.networking.wifi-info from project.yml entitlements
and NSLocationWhenInUseUsageDescription from Info.plist
- SSID detection gracefully returns nil without the capability; all UI handles
nil SSID already via optional chaining
feat: branding, ring polish, sign-out UX, version/support
- LoginView: replace title with "kisani." monospaced wordmark + UGREEN logo,
show username then NAS display name after login
- BackupView: "kisani." + UGREEN logo chip in toolbar; ring color is orange
(in-progress), green (completed), destructive (failed), dim (idle)
- BackupJob: cap progress at min(1.0, ...) to prevent >100% display
- SettingsView sign-out dialog: two actions — "Sign Out" (lock only, keeps NAS)
and "Sign Out & Forget NAS" (full clear); clearer message
- SettingsView ACCOUNT section: App Version v1.0 + Support mailto rows
- AppMenuView: About section with kisani. wordmark, version, support link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:39:35 +03:00
|
|
|
aboutSection
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.hPad)
|
|
|
|
|
.padding(.top, 8)
|
|
|
|
|
.padding(.bottom, 48)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.navigationTitle("Activity")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
|
|
Button("Done") { dismiss() }
|
|
|
|
|
.font(.system(size: 15, weight: .medium))
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 12:12:26 +03:00
|
|
|
.confirmationDialog("Clear Activity", isPresented: $showClearSheet, titleVisibility: .visible) {
|
|
|
|
|
Button("Clear Completed Logs") { store.clearCompletedHistory() }
|
|
|
|
|
if !errorEntries.isEmpty {
|
|
|
|
|
Button("Clear Errors") { store.clearErrorHistory() }
|
|
|
|
|
Button("Clear Everything", role: .destructive) { store.clearHistory() }
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
}
|
2026-05-17 12:12:26 +03:00
|
|
|
Button("Cancel", role: .cancel) {}
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
}
|
|
|
|
|
.sheet(item: $selectedErrorEntry) { entry in
|
|
|
|
|
errorDetailSheet(entry: entry)
|
|
|
|
|
}
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: — Connection Status
|
|
|
|
|
|
|
|
|
|
private var connectionStatusSection: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
menuSectionLabel("CONNECTION STATUS")
|
|
|
|
|
|
|
|
|
|
VStack(spacing: 0) {
|
2026-05-17 12:12:26 +03:00
|
|
|
// NAS / destination row
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
HStack(spacing: 12) {
|
2026-05-17 12:01:42 +03:00
|
|
|
iconBadge("server.rack", color: AppTheme.inkSecondary)
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text(store.savedConnection?.displayName ?? "No NAS connected")
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
if let conn = store.savedConnection {
|
2026-05-17 12:12:26 +03:00
|
|
|
Text("\(conn.nasProtocol.rawValue.uppercased()) · \(conn.host)")
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
2026-05-17 12:12:26 +03:00
|
|
|
if conn.remotePath != "/" {
|
|
|
|
|
Text(conn.remotePath)
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.truncationMode(.middle)
|
|
|
|
|
}
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
statusBadge(label: nasBadgeLabel, color: nasBadgeColor)
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
|
|
|
|
|
menuHairline
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
// Wi-Fi row
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
iconBadge("wifi", color: AppTheme.inkSecondary)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
Text("Wi-Fi")
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
Text(wifiSubtitle)
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
statusBadge(label: wifiBadgeLabel, color: wifiBadgeColor)
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
|
2026-05-18 23:42:43 +03:00
|
|
|
menuHairline
|
|
|
|
|
|
|
|
|
|
// Cellular row
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
iconBadge("antenna.radiowaves.left.and.right", color: lanMonitor.isOnCellular ? Color(red: 1.0, green: 0.58, blue: 0.0) : AppTheme.inkSecondary)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text("Cellular")
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
Text(lanMonitor.isOnCellular ? "Active" : "Not in use")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
statusBadge(
|
|
|
|
|
label: lanMonitor.isOnCellular ? "Active" : "Off",
|
|
|
|
|
color: lanMonitor.isOnCellular ? Color(red: 1.0, green: 0.58, blue: 0.0) : AppTheme.inkQuaternary
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
if lanMonitor.isTailscaleActive {
|
|
|
|
|
menuHairline
|
|
|
|
|
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
iconBadge("network.badge.shield.half.filled", color: AppTheme.interactive)
|
|
|
|
|
Text("Tailscale active")
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
Spacer()
|
|
|
|
|
statusBadge(label: "VPN", color: AppTheme.interactive)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
// MARK: — Current Session
|
|
|
|
|
|
|
|
|
|
private var currentSessionSection: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
menuSectionLabel("CURRENT SESSION")
|
|
|
|
|
|
|
|
|
|
VStack(spacing: 0) {
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
iconBadge(
|
|
|
|
|
engine.job.status == .running ? "arrow.up.circle" :
|
|
|
|
|
engine.job.status == .paused ? "pause.circle" : "arrow.triangle.2.circlepath",
|
|
|
|
|
color: sessionColor
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text(sessionTitle)
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
if engine.job.status == .running,
|
|
|
|
|
!engine.job.currentFileName.isEmpty {
|
|
|
|
|
Text(engine.job.currentFileName)
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.truncationMode(.middle)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
statusBadge(label: sessionBadgeLabel, color: sessionColor)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
|
|
|
|
|
if engine.job.totalFiles > 0 {
|
|
|
|
|
menuHairline
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
HStack {
|
|
|
|
|
Text("\(engine.job.uploadedFiles + engine.job.skippedFiles) of \(engine.job.totalFiles)")
|
|
|
|
|
.font(AppTheme.caption())
|
|
|
|
|
.foregroundStyle(AppTheme.inkSecondary)
|
|
|
|
|
Spacer()
|
|
|
|
|
if engine.job.speedBytesPerSecond > 1000 {
|
|
|
|
|
Text(speedString(engine.job.speedBytesPerSecond))
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GeometryReader { geo in
|
|
|
|
|
ZStack(alignment: .leading) {
|
|
|
|
|
RoundedRectangle(cornerRadius: 3, style: .continuous)
|
|
|
|
|
.fill(AppTheme.inkQuaternary.opacity(0.35))
|
|
|
|
|
.frame(height: 4)
|
|
|
|
|
RoundedRectangle(cornerRadius: 3, style: .continuous)
|
|
|
|
|
.fill(sessionColor)
|
|
|
|
|
.frame(width: max(0, geo.size.width * engine.job.progress), height: 4)
|
|
|
|
|
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: engine.job.progress)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.frame(height: 4)
|
|
|
|
|
|
|
|
|
|
if let eta = engine.job.eta, eta > 5 {
|
|
|
|
|
Text("About \(etaString(eta)) remaining")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 12)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: — Last Successful Backup
|
|
|
|
|
|
|
|
|
|
private var lastBackupSection: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
menuSectionLabel("LAST SUCCESSFUL BACKUP")
|
|
|
|
|
|
|
|
|
|
if let entry = lastSuccessfulEntry {
|
|
|
|
|
VStack(spacing: 0) {
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
iconBadge("checkmark.circle", color: AppTheme.positive)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text(entry.date.formatted(.dateTime.month(.abbreviated).day().hour().minute()))
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
let totalSafe = entry.uploadedCount + entry.skippedCount
|
|
|
|
|
Text("\(totalSafe) photo\(totalSafe == 1 ? "" : "s") safe")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
statusBadge(label: "Completed", color: AppTheme.positive)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
} else {
|
|
|
|
|
emptyCard(icon: "checkmark.circle", text: "No successful backups yet")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: — Errors
|
|
|
|
|
|
|
|
|
|
private var errorsSection: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
HStack {
|
|
|
|
|
menuSectionLabel("ERRORS")
|
|
|
|
|
Spacer()
|
2026-05-18 23:42:43 +03:00
|
|
|
if !errorEntries.isEmpty {
|
2026-05-17 12:12:26 +03:00
|
|
|
Button {
|
2026-05-18 23:42:43 +03:00
|
|
|
store.clearErrorHistory()
|
2026-05-17 12:12:26 +03:00
|
|
|
} label: {
|
2026-05-18 23:42:43 +03:00
|
|
|
Label("Clear Errors", systemImage: "trash")
|
2026-05-17 12:12:26 +03:00
|
|
|
.font(.system(size: 11, weight: .medium))
|
2026-05-18 23:42:43 +03:00
|
|
|
.foregroundStyle(AppTheme.destructive)
|
2026-05-17 12:12:26 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VStack(spacing: 0) {
|
|
|
|
|
ForEach(Array(errorEntries.prefix(3).enumerated()), id: \.element.id) { idx, entry in
|
|
|
|
|
Button {
|
|
|
|
|
selectedErrorEntry = entry
|
|
|
|
|
} label: {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
|
|
|
.font(.system(size: 12, weight: .regular))
|
|
|
|
|
.foregroundStyle(AppTheme.destructive)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text(entry.date.formatted(.dateTime.month(.abbreviated).day().hour().minute()))
|
|
|
|
|
.font(AppTheme.caption())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
Text("\(entry.failedCount) file\(entry.failedCount == 1 ? "" : "s") failed · \(entry.uploadedCount) uploaded")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.destructive.opacity(0.8))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
Image(systemName: "chevron.right")
|
|
|
|
|
.font(.system(size: 10, weight: .medium))
|
|
|
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 12)
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(ScaleButtonStyle())
|
|
|
|
|
|
|
|
|
|
if idx < min(errorEntries.count, 3) - 1 {
|
|
|
|
|
menuHairline
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
// MARK: — Sync Activity
|
|
|
|
|
|
|
|
|
|
private var syncActivitySection: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
HStack {
|
|
|
|
|
menuSectionLabel("SYNC ACTIVITY")
|
|
|
|
|
Spacer()
|
|
|
|
|
if !store.historyEntries.isEmpty {
|
2026-05-17 12:12:26 +03:00
|
|
|
Button {
|
|
|
|
|
showClearSheet = true
|
|
|
|
|
} label: {
|
|
|
|
|
Text("Clear ▾")
|
|
|
|
|
.font(.system(size: 12, weight: .medium))
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
}
|
|
|
|
|
}
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
|
|
|
|
|
VStack(spacing: 0) {
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
let total = store.historyEntries.count
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
let uploaded = store.historyEntries.reduce(0) { $0 + $1.uploadedCount }
|
2026-05-17 12:12:26 +03:00
|
|
|
let safe = store.historyEntries.reduce(0) { $0 + $1.skippedCount }
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
let errors = store.historyEntries.reduce(0) { $0 + $1.failedCount }
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
activityStat(icon: "clock.arrow.circlepath", label: "Backups completed", value: "\(total)")
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
menuHairline
|
2026-05-17 12:12:26 +03:00
|
|
|
activityStat(icon: "arrow.up.doc", label: "New uploads", value: "\(uploaded)")
|
|
|
|
|
menuHairline
|
|
|
|
|
activityStat(icon: "checkmark", label: "Already safe", value: "\(safe)")
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
|
|
|
|
|
if errors > 0 {
|
|
|
|
|
menuHairline
|
2026-05-17 12:12:26 +03:00
|
|
|
activityStat(icon: "exclamationmark.triangle", label: "Errors", value: "\(errors)", valueColor: AppTheme.destructive)
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: — History
|
|
|
|
|
|
|
|
|
|
private var historySection: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
menuSectionLabel("HISTORY")
|
|
|
|
|
|
|
|
|
|
if store.historyEntries.isEmpty {
|
|
|
|
|
emptyCard(icon: "clock.arrow.circlepath", text: "No backups yet")
|
|
|
|
|
} else {
|
|
|
|
|
VStack(spacing: 0) {
|
|
|
|
|
ForEach(Array(store.historyEntries.prefix(5).enumerated()), id: \.element.id) { idx, entry in
|
|
|
|
|
compactHistoryRow(entry)
|
|
|
|
|
if idx < min(store.historyEntries.count, 5) - 1 {
|
|
|
|
|
menuHairline
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if store.historyEntries.count > 5 {
|
|
|
|
|
menuHairline
|
|
|
|
|
Text("+ \(store.historyEntries.count - 5) more")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
|
|
|
.padding(.vertical, 10)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func compactHistoryRow(_ entry: BackupHistoryEntry) -> some View {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
Circle()
|
|
|
|
|
.fill(entry.failedCount > 0 ? AppTheme.destructive : AppTheme.positive)
|
|
|
|
|
.frame(width: 5, height: 5)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
2026-05-17 12:12:26 +03:00
|
|
|
Text(entry.date.formatted(.dateTime.month(.abbreviated).day().hour().minute()))
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
.font(AppTheme.caption())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
2026-05-17 12:12:26 +03:00
|
|
|
historySubtitle(entry)
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
if entry.triggeredByLAN {
|
|
|
|
|
Text("AUTO")
|
|
|
|
|
.font(.system(size: 9, weight: .semibold))
|
|
|
|
|
.foregroundStyle(AppTheme.inkSecondary)
|
|
|
|
|
.padding(.horizontal, 5)
|
|
|
|
|
.padding(.vertical, 2)
|
|
|
|
|
.overlay(
|
|
|
|
|
RoundedRectangle(cornerRadius: 3, style: .continuous)
|
|
|
|
|
.stroke(AppTheme.inkQuaternary, lineWidth: 0.75)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 12)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
@ViewBuilder
|
|
|
|
|
private func historySubtitle(_ entry: BackupHistoryEntry) -> some View {
|
|
|
|
|
if entry.failedCount > 0 {
|
|
|
|
|
Text("\(entry.uploadedCount) uploaded · \(entry.failedCount) failed")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.destructive.opacity(0.8))
|
|
|
|
|
} else if entry.uploadedCount == 0 && entry.skippedCount > 0 {
|
|
|
|
|
Text("\(entry.skippedCount) already backed up")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
} else if entry.skippedCount > 0 {
|
|
|
|
|
Text("\(entry.uploadedCount) new · \(entry.skippedCount) already safe")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
} else {
|
|
|
|
|
Text("\(entry.uploadedCount) photo\(entry.uploadedCount == 1 ? "" : "s") backed up")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
// MARK: — Error Detail Sheet
|
|
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
|
private func errorDetailSheet(entry: BackupHistoryEntry) -> some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
ZStack {
|
|
|
|
|
AppTheme.background.ignoresSafeArea()
|
|
|
|
|
ScrollView {
|
|
|
|
|
VStack(spacing: AppTheme.sectionGap) {
|
2026-05-17 12:12:26 +03:00
|
|
|
// Summary card
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
VStack(spacing: 0) {
|
|
|
|
|
detailRow(label: "Date", value: entry.date.formatted(.dateTime.month(.wide).day().year().hour().minute()))
|
|
|
|
|
menuHairline
|
|
|
|
|
detailRow(label: "Failed", value: "\(entry.failedCount) file\(entry.failedCount == 1 ? "" : "s")", valueColor: AppTheme.destructive)
|
|
|
|
|
menuHairline
|
|
|
|
|
detailRow(label: "Uploaded", value: "\(entry.uploadedCount) file\(entry.uploadedCount == 1 ? "" : "s")")
|
|
|
|
|
menuHairline
|
2026-05-17 12:12:26 +03:00
|
|
|
detailRow(label: "Already safe", value: "\(entry.skippedCount) file\(entry.skippedCount == 1 ? "" : "s")")
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
menuHairline
|
|
|
|
|
detailRow(label: "Duration", value: durationString(entry.durationSeconds))
|
|
|
|
|
menuHairline
|
|
|
|
|
detailRow(label: "NAS", value: entry.nasHost)
|
|
|
|
|
menuHairline
|
|
|
|
|
detailRow(label: "Triggered by", value: entry.triggeredByLAN ? "LAN (automatic)" : "Manual")
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
// Suggested fix
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
menuSectionLabel("SUGGESTED FIX")
|
|
|
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
|
|
|
fixRow(icon: "network", text: "Ensure the device is on the same network as the NAS.")
|
|
|
|
|
fixRow(icon: "internaldrive", text: "Check that the NAS has enough available storage space.")
|
2026-05-17 12:12:26 +03:00
|
|
|
fixRow(icon: "arrow.clockwise", text: "Retry — already-uploaded photos are skipped automatically.")
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
}
|
|
|
|
|
.padding(AppTheme.cardPad)
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
2026-05-17 12:12:26 +03:00
|
|
|
|
|
|
|
|
// Retry button
|
|
|
|
|
Button {
|
|
|
|
|
selectedErrorEntry = nil
|
|
|
|
|
retryBackup()
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Retry Failed Uploads", systemImage: "arrow.clockwise")
|
|
|
|
|
.font(.system(size: 15, weight: .semibold))
|
|
|
|
|
.foregroundStyle(.white)
|
|
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
|
.frame(height: 50)
|
|
|
|
|
.background(
|
|
|
|
|
lanMonitor.nasReachable == true
|
|
|
|
|
? AppTheme.interactive
|
|
|
|
|
: AppTheme.inkQuaternary
|
|
|
|
|
)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
|
|
|
}
|
|
|
|
|
.disabled(lanMonitor.nasReachable != true || engine.job.status.isActive)
|
|
|
|
|
.buttonStyle(ScaleButtonStyle())
|
|
|
|
|
|
|
|
|
|
if lanMonitor.nasReachable != true {
|
|
|
|
|
Text("NAS is not reachable — connect to the same network first.")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
|
|
|
.padding(.top, -4)
|
|
|
|
|
}
|
Fix AppMenuView Connection Status UX and add smart sync dashboard
- Wi-Fi row: title always "Wi-Fi", subtitle shows SSID or Cellular/Not connected;
badge is Connected (green) / Cellular (amber) / Offline (muted) — no more
contradictory "No Wi-Fi" title with "Wi-Fi connected" subtitle
- NAS badge: Reachable now amber (TCP ping only, not auth-confirmed), Offline red,
Unknown muted — removes misleading green on unverified NAS connectivity
- Error rows are now tappable; chevron affordance + ScaleButtonStyle; opens detail
sheet with date, counts, duration, NAS host, trigger type, and suggested fix steps
- Sync Activity section: Clear button with confirmation alert (warns about error count
if errors exist), calls store.clearHistory()
- BackupViewModel: compareWithNAS() computes accurate pre-scan alreadySafeCount /
notBackedUpCount by matching lowercased filenames against NAS directory listing
- BackupView: ring shows state-based content (idle: not-backed-up count, running: %,
completed: checkmark), pill-style page indicator, display counts update live during
backup run, loadNASAndCompare() called on appear
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
2026-05-17 11:54:43 +03:00
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.hPad)
|
|
|
|
|
.padding(.top, 8)
|
|
|
|
|
.padding(.bottom, 48)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.navigationTitle("Error Detail")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
|
|
Button("Done") { selectedErrorEntry = nil }
|
|
|
|
|
.font(.system(size: 15, weight: .medium))
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix(signing): remove Access Wi-Fi entitlement (unsupported on free teams)
- Drop com.apple.developer.networking.wifi-info from project.yml entitlements
and NSLocationWhenInUseUsageDescription from Info.plist
- SSID detection gracefully returns nil without the capability; all UI handles
nil SSID already via optional chaining
feat: branding, ring polish, sign-out UX, version/support
- LoginView: replace title with "kisani." monospaced wordmark + UGREEN logo,
show username then NAS display name after login
- BackupView: "kisani." + UGREEN logo chip in toolbar; ring color is orange
(in-progress), green (completed), destructive (failed), dim (idle)
- BackupJob: cap progress at min(1.0, ...) to prevent >100% display
- SettingsView sign-out dialog: two actions — "Sign Out" (lock only, keeps NAS)
and "Sign Out & Forget NAS" (full clear); clearer message
- SettingsView ACCOUNT section: App Version v1.0 + Support mailto rows
- AppMenuView: About section with kisani. wordmark, version, support link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:39:35 +03:00
|
|
|
// MARK: — About
|
|
|
|
|
|
|
|
|
|
private var aboutSection: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
menuSectionLabel("ABOUT")
|
|
|
|
|
|
|
|
|
|
VStack(spacing: 0) {
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
Text("kisani.")
|
|
|
|
|
.font(.system(size: 13, weight: .medium, design: .monospaced))
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
.kerning(0.5)
|
|
|
|
|
Spacer()
|
|
|
|
|
Text("v1.0")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
|
|
|
|
|
menuHairline
|
|
|
|
|
|
|
|
|
|
Link(destination: URL(string: "mailto:apps@provoc.ug")!) {
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
Image(systemName: "envelope")
|
|
|
|
|
.font(.system(size: 13, weight: .regular))
|
|
|
|
|
.foregroundStyle(AppTheme.inkSecondary)
|
|
|
|
|
.frame(width: 20)
|
|
|
|
|
Text("Support")
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
Spacer()
|
|
|
|
|
Text("apps@provoc.ug")
|
|
|
|
|
.font(AppTheme.micro())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
Image(systemName: "arrow.up.right")
|
|
|
|
|
.font(.system(size: 10, weight: .medium))
|
|
|
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 14)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 12:12:26 +03:00
|
|
|
// MARK: — Actions
|
|
|
|
|
|
|
|
|
|
private func retryBackup() {
|
|
|
|
|
guard let conn = store.savedConnection, !engine.job.status.isActive else { return }
|
|
|
|
|
dismiss()
|
|
|
|
|
Task { _ = try? await engine.run(connection: conn, filter: store.backupFilter) }
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
// MARK: — Helpers
|
|
|
|
|
|
|
|
|
|
private func menuSectionLabel(_ text: String) -> some View {
|
|
|
|
|
Text(text)
|
|
|
|
|
.font(.system(size: 11, weight: .semibold))
|
|
|
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
|
|
|
.kerning(0.6)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var menuHairline: some View {
|
|
|
|
|
Rectangle()
|
|
|
|
|
.fill(AppTheme.separator)
|
|
|
|
|
.frame(height: 0.5)
|
|
|
|
|
.padding(.leading, AppTheme.cardPad)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func iconBadge(_ name: String, color: Color) -> some View {
|
|
|
|
|
ZStack {
|
|
|
|
|
Circle()
|
|
|
|
|
.fill(AppTheme.surfaceSunken)
|
|
|
|
|
.frame(width: 32, height: 32)
|
|
|
|
|
Image(systemName: name)
|
|
|
|
|
.font(.system(size: 13, weight: .medium))
|
|
|
|
|
.foregroundStyle(color)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func statusBadge(label: String, color: Color) -> some View {
|
|
|
|
|
HStack(spacing: 4) {
|
|
|
|
|
Circle().fill(color).frame(width: 5, height: 5)
|
|
|
|
|
Text(label)
|
|
|
|
|
.font(.system(size: 11, weight: .medium))
|
|
|
|
|
.foregroundStyle(color)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 8)
|
|
|
|
|
.padding(.vertical, 4)
|
|
|
|
|
.background(color.opacity(0.08))
|
|
|
|
|
.clipShape(Capsule(style: .continuous))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func activityStat(icon: String, label: String, value: String, valueColor: Color = AppTheme.ink) -> some View {
|
|
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
Image(systemName: icon)
|
|
|
|
|
.font(.system(size: 13, weight: .regular))
|
|
|
|
|
.foregroundStyle(AppTheme.inkSecondary)
|
|
|
|
|
.frame(width: 20)
|
|
|
|
|
Text(label)
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
|
Spacer()
|
|
|
|
|
Text(value)
|
|
|
|
|
.font(.system(size: 14, weight: .semibold))
|
|
|
|
|
.foregroundStyle(valueColor)
|
|
|
|
|
.monospacedDigit()
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 13)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func emptyCard(icon: String, text: String) -> some View {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
Image(systemName: icon)
|
|
|
|
|
.font(.system(size: 13, weight: .light))
|
|
|
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
|
|
|
Text(text)
|
|
|
|
|
.font(AppTheme.caption())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
}
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 16)
|
|
|
|
|
.background(AppTheme.surfaceRaised)
|
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
|
|
|
|
|
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
|
|
|
|
|
}
|
2026-05-17 12:12:26 +03:00
|
|
|
|
|
|
|
|
private func detailRow(label: String, value: String, valueColor: Color = AppTheme.ink) -> some View {
|
|
|
|
|
HStack {
|
|
|
|
|
Text(label)
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
|
|
|
Spacer()
|
|
|
|
|
Text(value)
|
|
|
|
|
.font(AppTheme.body())
|
|
|
|
|
.foregroundStyle(valueColor)
|
|
|
|
|
.multilineTextAlignment(.trailing)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, AppTheme.cardPad)
|
|
|
|
|
.padding(.vertical, 13)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func fixRow(icon: String, text: String) -> some View {
|
|
|
|
|
HStack(alignment: .top, spacing: 10) {
|
|
|
|
|
Image(systemName: icon)
|
|
|
|
|
.font(.system(size: 12, weight: .regular))
|
|
|
|
|
.foregroundStyle(AppTheme.inkSecondary)
|
|
|
|
|
.frame(width: 16, height: 16)
|
|
|
|
|
.padding(.top, 1)
|
|
|
|
|
Text(text)
|
|
|
|
|
.font(AppTheme.caption())
|
|
|
|
|
.foregroundStyle(AppTheme.inkSecondary)
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func speedString(_ bytesPerSecond: Double) -> String {
|
|
|
|
|
if bytesPerSecond >= 1_048_576 {
|
|
|
|
|
return String(format: "%.1f MB/s", bytesPerSecond / 1_048_576)
|
|
|
|
|
}
|
|
|
|
|
return String(format: "%.0f KB/s", bytesPerSecond / 1_024)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func etaString(_ seconds: TimeInterval) -> String {
|
|
|
|
|
let mins = Int(seconds) / 60
|
|
|
|
|
let secs = Int(seconds) % 60
|
|
|
|
|
if mins >= 60 { return "\(mins / 60)h \(mins % 60)m" }
|
|
|
|
|
if mins > 0 { return "\(mins) min" }
|
|
|
|
|
return "\(secs)s"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func durationString(_ seconds: Double) -> String {
|
|
|
|
|
let m = Int(seconds) / 60
|
|
|
|
|
let s = Int(seconds) % 60
|
|
|
|
|
if seconds < 60 { return "\(Int(seconds))s" }
|
|
|
|
|
return s == 0 ? "\(m)m" : "\(m)m \(s)s"
|
|
|
|
|
}
|
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>
2026-05-16 17:58:00 +03:00
|
|
|
}
|