diff --git a/Assets.xcassets/nas_logo_clean.imageset/Contents.json b/Assets.xcassets/nas_logo_clean.imageset/Contents.json index f695d75..0a56906 100644 --- a/Assets.xcassets/nas_logo_clean.imageset/Contents.json +++ b/Assets.xcassets/nas_logo_clean.imageset/Contents.json @@ -6,10 +6,12 @@ "scale" : "1x" }, { + "filename" : "nas_logo_clean.png", "idiom" : "universal", "scale" : "2x" }, { + "filename" : "nas_logo_clean.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Features/Login/LoginView.swift b/Features/Login/LoginView.swift index f8ad362..6773037 100644 --- a/Features/Login/LoginView.swift +++ b/Features/Login/LoginView.swift @@ -5,6 +5,8 @@ struct LoginView: View { @StateObject private var vm = LoginViewModel() @State private var appeared = false + private var connection: NASConnection? { store.savedConnection } + var body: some View { ZStack { AppTheme.background.ignoresSafeArea() @@ -12,46 +14,54 @@ struct LoginView: View { VStack(spacing: 0) { Spacer() - // Logo + wordmark - VStack(spacing: 20) { - Image("nas_logo_clean") - .resizable() - .scaledToFit() - .frame(width: 80, height: 80) - .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) - .shadow(color: .black.opacity(0.12), radius: 20, x: 0, y: 6) - .scaleEffect(appeared ? 1 : 0.85) + // ── App identity ────────────────────────── + VStack(spacing: 6) { + Text("Kisani") + .font(.system(size: 34, weight: .bold, design: .default)) + .foregroundStyle(AppTheme.ink) .opacity(appeared ? 1 : 0) - .animation(.spring(response: 0.5, dampingFraction: 0.72).delay(0.08), value: appeared) - - VStack(spacing: 6) { - Text("Kisani") - .font(.system(size: 32, weight: .bold, design: .default)) - .foregroundStyle(AppTheme.ink) - .opacity(appeared ? 1 : 0) - .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.16), value: appeared) - - Text("Your NAS, always in sync.") - .font(.system(size: 15, weight: .regular)) - .foregroundStyle(AppTheme.inkTertiary) - .opacity(appeared ? 1 : 0) - .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.22), value: appeared) - } + .offset(y: appeared ? 0 : 8) + .animation(.spring(response: 0.44, dampingFraction: 0.82).delay(0.06), value: appeared) } - Spacer().frame(height: 52) + Spacer().frame(height: 28) - // Auth button + error - VStack(spacing: 12) { + // ── Logo ────────────────────────────────── + logoView + .scaleEffect(appeared ? 1 : 0.88) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.48, dampingFraction: 0.74).delay(0.12), value: appeared) + + Spacer().frame(height: 24) + + // ── Session context ─────────────────────── + if let conn = connection { + VStack(spacing: 5) { + Text(conn.displayName) + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(AppTheme.ink) + Text(conn.username) + .font(.system(size: 14, weight: .regular)) + .foregroundStyle(AppTheme.inkTertiary) + } + .opacity(appeared ? 1 : 0) + .offset(y: appeared ? 0 : 6) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.18), value: appeared) + } + + Spacer().frame(height: 44) + + // ── Auth ────────────────────────────────── + VStack(spacing: 14) { Button(action: { vm.authenticateWithFaceID() }) { - HStack(spacing: 8) { + HStack(spacing: 9) { if vm.isAuthenticating { ProgressView() .tint(AppTheme.inkInverse) .scaleEffect(0.85) } else { Image(systemName: "faceid") - .font(.system(size: 17, weight: .medium)) + .font(.system(size: 18, weight: .medium)) Text("Sign in with Face ID") .font(.system(size: 15, weight: .semibold)) } @@ -61,107 +71,59 @@ struct LoginView: View { .padding(.horizontal, AppTheme.hPad) .disabled(vm.isAuthenticating) .opacity(appeared ? 1 : 0) - .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.28), value: appeared) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.24), value: appeared) if let error = vm.authError { HStack(spacing: 5) { Image(systemName: "exclamationmark.circle") - .font(.system(size: 12)) + .font(.system(size: 12, weight: .regular)) Text(error) .font(AppTheme.caption()) } .foregroundStyle(AppTheme.destructive) - .transition(.opacity.combined(with: .move(edge: .top))) + .transition(.asymmetric( + insertion: .opacity.combined(with: .move(edge: .top)), + removal: .opacity + )) } } - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.authError != nil) + .animation(.spring(response: 0.3, dampingFraction: 0.85), value: vm.authError != nil) Spacer() - // More options - Button("More options") { vm.showMoreSheet = true } - .font(.system(size: 13, weight: .regular)) - .foregroundStyle(AppTheme.inkTertiary) - .padding(.bottom, 48) - .opacity(appeared ? 1 : 0) - .animation(.spring(response: 0.45, dampingFraction: 0.8).delay(0.36), value: appeared) + // ── Switch user ─────────────────────────── + Button(action: { store.signOut() }) { + Text("Switch User") + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(AppTheme.inkTertiary) + } + .buttonStyle(ScaleButtonStyle()) + .padding(.bottom, 52) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.32), value: appeared) } } - .sheet(isPresented: $vm.showMoreSheet) { - MoreOptionsSheet(isPresented: $vm.showMoreSheet) { - vm.showMoreSheet = false - store.isSessionActive = true - } - .presentationDetents([.height(200)]) - .modifier(SheetCornerRadius(20)) - } .onAppear { vm.onAuthenticated = { store.isSessionActive = true } appeared = true } } -} -struct MoreOptionsSheet: View { - @Binding var isPresented: Bool - var onAuthenticated: (() -> Void)? + // MARK: — Logo - var body: some View { - VStack(spacing: 0) { - Capsule() - .fill(AppTheme.inkQuaternary) - .frame(width: 32, height: 4) - .padding(.top, 12) - .padding(.bottom, 20) + private var logoView: some View { + ZStack { + // Subtle ring so logo edge is always legible against any background + RoundedRectangle(cornerRadius: 24, style: .continuous) + .stroke(AppTheme.inkQuaternary, lineWidth: 0.5) + .frame(width: 92, height: 92) - VStack(spacing: 0) { - sheetRow("Password", weight: .medium) { } - Divider().padding(.leading, 16) - sheetRow("Another account") { - isPresented = false - onAuthenticated?() - } - Divider().padding(.leading, 16) - sheetRow("Cancel", color: AppTheme.inkTertiary) { isPresented = false } - } - .background(AppTheme.surfaceRaised) - .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous) - .stroke(AppTheme.ink.opacity(0.08), lineWidth: 0.5) - ) - .padding(.horizontal, AppTheme.hPad) - .padding(.bottom, 16) - } - .background(AppTheme.background) - } - - private func sheetRow( - _ title: String, - weight: Font.Weight = .regular, - color: Color = AppTheme.ink, - action: @escaping () -> Void - ) -> some View { - Button(action: action) { - Text(title) - .font(.system(size: 15, weight: weight)) - .foregroundStyle(color) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16) - .padding(.vertical, 14) - } - .buttonStyle(ScaleButtonStyle()) - } -} - -private struct SheetCornerRadius: ViewModifier { - let radius: CGFloat - init(_ radius: CGFloat) { self.radius = radius } - func body(content: Content) -> some View { - if #available(iOS 16.4, *) { - content.presentationCornerRadius(radius) - } else { - content + Image("nas_logo_clean") + .resizable() + .scaledToFit() + .frame(width: 90, height: 90) + .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) } + .shadow(color: .black.opacity(0.14), radius: 20, x: 0, y: 6) } } diff --git a/Features/Settings/SettingsView.swift b/Features/Settings/SettingsView.swift index ad986cb..f7fb087 100644 --- a/Features/Settings/SettingsView.swift +++ b/Features/Settings/SettingsView.swift @@ -345,10 +345,4 @@ struct SettingsView: View { } } - private func sectionLabel(_ text: String) -> some View { - Text(text) - .font(AppTheme.micro()) - .foregroundStyle(AppTheme.inkTertiary) - .padding(.leading, 4) - } } diff --git a/Shared/Persistence/ConnectionStore.swift b/Shared/Persistence/ConnectionStore.swift index 63ea4cb..7659799 100644 --- a/Shared/Persistence/ConnectionStore.swift +++ b/Shared/Persistence/ConnectionStore.swift @@ -104,6 +104,12 @@ final class ConnectionStore: ObservableObject { UserDefaults.standard.removeObject(forKey: "historyEntries") } + /// Locks the screen — keeps the saved connection but requires Face ID again. + func lock() { + isSessionActive = false + } + + /// Clears everything: connection, session, and onboarding state. Returns to ConnectView. func signOut() { isSessionActive = false onboardingPhotoShown = false