feat: Kisani rebrand, step-based onboarding flow, dark mode fixes

- Rename app to Kisani in all UI text and Face ID prompt
- Replace flat 2-state RootView with 5-step flow: ConnectView →
  LoginView → FolderSetupView → PhotoPermissionView → MainTabView
- Remove NavigationStack from LoginView (was causing nested-stack crash);
  drive navigation via ConnectionStore.isSessionActive
- Add FolderSetupView and PhotoPermissionView onboarding screens with
  step indicator (2/3, 3/3), spring entrance animations, and UGreen
  connection chip
- Fix dark mode button invisibility: PrimaryButtonStyle and PillButtonStyle
  now use AppTheme.inkInverse; GhostButtonStyle border uses ink.opacity(0.25)
- Add AppTheme.inkInverse adaptive token (dark bg ink on dark, white on light)
- Add ConnectionStore.isSessionActive (non-persisted) and
  onboardingPhotoShown (UserDefaults-persisted)
- Add os_log logging in LoginViewModel at auth start/success/failure
- Remove dead Help button from ConnectView
- UGreenCompatibilityBadge, KeychainStore, SavedConnections, extensions,
  PrivacyInfo.xcprivacy, unit test target and test stubs
- AppTheme: full UIColor dynamic provider tokens for dark/light adaptive color
- AppearanceMode enum + segmented picker in SettingsView
- BackupView: enlarged ring, removed options card and linear progress bar
- HistoryView: duration and bytes formatting, improved typography hierarchy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-05-16 16:20:53 +03:00
parent 03d63e60de
commit ae38f6e417
38 changed files with 1649 additions and 451 deletions

View File

@@ -131,7 +131,7 @@ struct LANTriggerSection: View {
.padding(.horizontal, AppTheme.cardPad)
.padding(.vertical, 12)
}
.background(Color.white)
.background(AppTheme.surfaceRaised)
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
}

View File

@@ -6,10 +6,11 @@ struct SettingsView: View {
var body: some View {
ZStack {
Color.white.ignoresSafeArea()
AppTheme.background.ignoresSafeArea()
ScrollView {
VStack(spacing: AppTheme.sectionGap) {
appearanceSection
LANTriggerSection()
mediaSection
quietHoursSection
@@ -26,6 +27,45 @@ struct SettingsView: View {
.navigationBarTitleDisplayMode(.inline)
}
// MARK: Appearance
private var appearanceSection: some View {
VStack(alignment: .leading, spacing: 8) {
sectionLabel("APPEARANCE")
HStack(spacing: 0) {
ForEach(AppearanceMode.allCases, id: \.self) { mode in
Button {
withAnimation(.spring(response: 0.25, dampingFraction: 0.8)) {
store.appearanceMode = mode
}
} label: {
let selected = store.appearanceMode == mode
HStack(spacing: 5) {
Image(systemName: mode.icon)
.font(.system(size: 12, weight: selected ? .semibold : .regular))
Text(mode.label)
.font(.system(size: 12, weight: selected ? .semibold : .regular))
}
.foregroundStyle(selected ? AppTheme.ink : AppTheme.inkTertiary)
.frame(maxWidth: .infinity)
.padding(.vertical, 9)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(selected ? AppTheme.surfaceRaised : Color.clear)
.shadow(color: selected ? .black.opacity(0.08) : .clear,
radius: 4, x: 0, y: 1)
)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(3)
.background(AppTheme.surfaceSunken)
.clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous))
}
}
// MARK: Media
private var mediaSection: some View {
@@ -49,7 +89,7 @@ struct SettingsView: View {
subtitle: "Skip files already on NAS",
isOn: $store.backupFilter.newFilesOnly)
}
.background(Color.white)
.background(AppTheme.surfaceRaised)
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
}
@@ -78,7 +118,7 @@ struct SettingsView: View {
subtitle: "Preserve battery on large libraries",
isOn: $store.chargingOnlyMode)
}
.background(Color.white)
.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: store.quietHoursEnabled)
@@ -135,7 +175,7 @@ struct SettingsView: View {
.padding(.vertical, 10)
}
}
.background(Color.white)
.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.isOnCellular)
@@ -206,13 +246,15 @@ struct SettingsView: View {
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
}
.buttonStyle(ScaleButtonStyle())
.accessibilityLabel("Open Tailscale app")
.accessibilityHint("Opens the Tailscale app to connect the VPN")
}
}
.padding(.horizontal, AppTheme.cardPad)
.padding(.vertical, 12)
}
}
.background(Color.white)
.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: store.useTailscaleWhenRemote)
@@ -233,7 +275,7 @@ struct SettingsView: View {
hairline
notifRow(icon: "exclamationmark.triangle", title: "On errors", value: "On")
}
.background(Color.white)
.background(AppTheme.surfaceRaised)
.clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous))
.shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2)
}