diff --git a/AGENTS.md b/AGENTS.md index 1553462..de5d8ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -553,7 +553,6 @@ SelfieCam uses the following architectural patterns: - Skin smoothing - Center Stage - Extended timers (5s, 10s) -- Video and Boomerang capture modes ## Settings & iCloud Sync @@ -621,7 +620,6 @@ var flashMode: CameraFlashMode // .off, .on, .auto - Front/back camera switching - Pinch-to-zoom - Photo capture with quality settings -- Video recording (premium) - HDR mode (premium) diff --git a/AI_Implementation.md b/AI_Implementation.md index ab171bb..e78900e 100644 --- a/AI_Implementation.md +++ b/AI_Implementation.md @@ -75,7 +75,6 @@ Features/ - Post-capture preview with share functionality - Auto-save option to Photo Library - Front flash using screen brightness -- Support for photo, video, and boomerang modes ### 4. Freemium Model - Built with **RevenueCat** for subscription management @@ -128,7 +127,6 @@ var isMirrorFlipped: Bool { | Skin smoothing | Off | Configurable | | Flash sync | Off | Configurable | | Center stage | Off | Configurable | -| Capture modes | Photo | Photo, Video, Boomerang | --- diff --git a/README.md b/README.md index 26ccce1..bc15201 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,8 @@ Perfect for low-light selfies, content creation, video calls, makeup application - **Front Flash**: Uses screen brightness for front camera flash effect - Real-time camera preview with smooth performance -### Capture Modes +### Capture - **Photo capture** with high-quality output -- **Video recording** (Premium) -- **Boomerang mode** for looping short videos (Premium) - Self-timer with 3-second (free), 5-second, and 10-second (Premium) options - Pinch-to-zoom gesture support - Rule-of-thirds grid overlay (toggleable) @@ -34,7 +32,6 @@ Perfect for low-light selfies, content creation, video calls, makeup application - **Skin Smoothing**: Real-time subtle skin smoothing filter - **Center Stage**: Automatic subject tracking/centering - **Extended Timers**: 5-second and 10-second self-timer options -- **Video & Boomerang**: Video recording and looping video capture - Ad-free experience ### iCloud Sync @@ -139,15 +136,15 @@ Add `REVENUECAT_API_KEY` as a secret in your Xcode Cloud workflow. ## Privacy - Camera access required for preview and capture -- Photo Library access required to save photos/videos -- Microphone access required for video recording +- Photo Library access required to save photos +- Microphone access may be requested by the camera framework (not actively used) - iCloud access for settings synchronization (optional) - No data collection, no analytics, no tracking ## Monetization Freemium model with optional "Pro" subscription: - **Free**: Basic ring light, standard colors (Pure White, Warm Cream), photo capture, 3s timer, grid, zoom -- **Pro**: Full color palette, custom colors, HDR, high quality, flash sync, true mirror, skin smoothing, center stage, extended timers, video, boomerang +- **Pro**: Full color palette, custom colors, HDR, high quality, flash sync, true mirror, skin smoothing, center stage, extended timers Implemented with RevenueCat for reliable subscription management. diff --git a/SelfieCam.xcodeproj/project.pbxproj b/SelfieCam.xcodeproj/project.pbxproj index a506c20..6c9df86 100644 --- a/SelfieCam.xcodeproj/project.pbxproj +++ b/SelfieCam.xcodeproj/project.pbxproj @@ -422,9 +422,9 @@ DEVELOPMENT_TEAM = 6R7KLBPBLZ; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos and videos, and enable advanced features like Center Stage auto-framing."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access to record clear audio when capturing videos, ensuring your voiceovers and ambient sounds are captured along with your video content."; - INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos and videos to your device, making them available in the Photos app and other compatible applications."; + INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos, and enable advanced features like Center Stage auto-framing."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access for the camera framework to initialize properly. Audio is not recorded."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos to your device, making them available in the Photos app and other compatible applications."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground; @@ -459,9 +459,9 @@ DEVELOPMENT_TEAM = 6R7KLBPBLZ; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos and videos, and enable advanced features like Center Stage auto-framing."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access to record clear audio when capturing videos, ensuring your voiceovers and ambient sounds are captured along with your video content."; - INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos and videos to your device, making them available in the Photos app and other compatible applications."; + INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos, and enable advanced features like Center Stage auto-framing."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access for the camera framework to initialize properly. Audio is not recorded."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos to your device, making them available in the Photos app and other compatible applications."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground; diff --git a/SelfieCam/Features/Camera/ContentView.swift b/SelfieCam/Features/Camera/ContentView.swift index 61576ec..5819692 100644 --- a/SelfieCam/Features/Camera/ContentView.swift +++ b/SelfieCam/Features/Camera/ContentView.swift @@ -39,7 +39,7 @@ struct ContentView: View { .ignoresSafeArea() // Only camera ignores safe area to fill screen } - // Photo review overlay + // Photo review overlay - handles its own safe area if showPhotoReview, let photo = capturedPhoto { PhotoReviewView( photo: photo, @@ -53,24 +53,25 @@ struct ContentView: View { } ) .transition(.opacity) - .ignoresSafeArea() // Photo review also fills screen } } - // Settings button overlay - respects safe area naturally + // Settings button overlay - only show when NOT in photo review mode .overlay(alignment: .topTrailing) { - Button { - showSettings = true - } label: { - Image(systemName: "gearshape.fill") - .font(.title3) - .foregroundStyle(.white) - .padding(Design.Spacing.medium) - .background(.ultraThinMaterial, in: Circle()) - .shadow(radius: Design.Shadow.radiusSmall) + if !showPhotoReview { + Button { + showSettings = true + } label: { + Image(systemName: "gearshape.fill") + .font(.title3) + .foregroundStyle(.white) + .padding(Design.Spacing.medium) + .background(.ultraThinMaterial, in: Circle()) + .shadow(radius: Design.Shadow.radiusSmall) + } + .accessibilityLabel("Settings") + .padding(.horizontal, Design.Spacing.large) + .padding(.top, Design.Spacing.small) } - .accessibilityLabel("Settings") - .padding(.horizontal, Design.Spacing.large) - .padding(.top, Design.Spacing.small) } .animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview) .onAppear { diff --git a/SelfieCam/Features/Camera/Views/PhotoReviewView.swift b/SelfieCam/Features/Camera/Views/PhotoReviewView.swift index f96778e..7fb2513 100644 --- a/SelfieCam/Features/Camera/Views/PhotoReviewView.swift +++ b/SelfieCam/Features/Camera/Views/PhotoReviewView.swift @@ -1,6 +1,6 @@ // // PhotoReviewView.swift -// CameraTester +// SelfieCam // // Created by Matt Bruce on 1/2/26. // @@ -16,58 +16,81 @@ struct PhotoReviewView: View { let saveError: String? let onRetake: () -> Void let onSave: () -> Void + + // Layout constants + private let toolbarHeight: CGFloat = 100 + private let topButtonSize: CGFloat = 44 // Retake, Close (smaller, out of the way) + private let bottomButtonSize: CGFloat = 64 // Share, Save (larger, main actions) var body: some View { ZStack { - // Photo display + // Black background Color.black .ignoresSafeArea() + // Photo - centered in available space Image(uiImage: photo.image) .resizable() .scaledToFit() .ignoresSafeArea() - - // Top toolbar - VStack { + } + // Top toolbar with gradient background + .overlay(alignment: .top) { + ZStack(alignment: .bottom) { + // Gradient background for visibility + LinearGradient( + colors: [Color.black.opacity(0.7), Color.black.opacity(0)], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: toolbarHeight) + .ignoresSafeArea(edges: .top) + + // Buttons HStack { // Retake button Button(action: onRetake) { Image(systemName: "arrow.triangle.2.circlepath") - .font(.system(size: 20, weight: .medium)) - .foregroundColor(.white) - .frame(width: 44, height: 44) - .background( - Circle() - .fill(Color.black.opacity(0.6)) - ) + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: topButtonSize, height: topButtonSize) + .background(.ultraThinMaterial, in: Circle()) } + .accessibilityLabel("Retake photo") Spacer() // Close button Button(action: onRetake) { Image(systemName: "xmark") - .font(.system(size: 20, weight: .medium)) - .foregroundColor(.white) - .frame(width: 44, height: 44) - .background( - Circle() - .fill(Color.black.opacity(0.6)) - ) + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: topButtonSize, height: topButtonSize) + .background(.ultraThinMaterial, in: Circle()) } + .accessibilityLabel("Close") } .padding(.horizontal, Design.Spacing.large) - .padding(.top, Design.Spacing.large) - - Spacer() - - // Bottom action bar + .padding(.bottom, Design.Spacing.medium) + } + } + // Bottom toolbar with gradient background + .overlay(alignment: .bottom) { + ZStack(alignment: .top) { + // Gradient background for visibility + LinearGradient( + colors: [Color.black.opacity(0), Color.black.opacity(0.85)], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: 160) + .ignoresSafeArea(edges: .bottom) + VStack(spacing: Design.Spacing.medium) { // Save status or error if let error = saveError { Text(error) - .foregroundColor(.red) + .foregroundStyle(.red) .font(.system(size: 14, weight: .medium)) .multilineTextAlignment(.center) .padding(.vertical, Design.Spacing.small) @@ -81,38 +104,37 @@ struct PhotoReviewView: View { ProgressView() .tint(.white) Text("Saving...") - .foregroundColor(.white) + .foregroundStyle(.white) .font(.system(size: 16, weight: .medium)) } - .padding(.vertical, Design.Spacing.small) } - // Action buttons - HStack(spacing: Design.Spacing.xLarge) { - // Share button + // Action buttons - same size, different styling + HStack(spacing: Design.Spacing.xLarge * 2) { + // Share button (frosted glass = secondary) ShareButton(photo: photo.image) + .frame(width: bottomButtonSize, height: bottomButtonSize) + .background(.ultraThinMaterial, in: Circle()) - // Save button + // Save button (solid white = primary) Button(action: onSave) { - ZStack { - Circle() - .fill(Color.white) - .frame(width: 80, height: 80) - - Image(systemName: "checkmark") - .font(.system(size: 24, weight: .bold)) - .foregroundColor(.black) - } - .shadow(radius: 5) + Image(systemName: "checkmark") + .font(.system(size: 26, weight: .bold)) + .foregroundStyle(.black) + .frame(width: bottomButtonSize, height: bottomButtonSize) + .background(.white, in: Circle()) + .shadow(color: .black.opacity(0.3), radius: Design.Shadow.radiusMedium) } .disabled(isSaving) + .accessibilityLabel("Save photo") + .accessibilityHint("Saves the photo to your library") } - .padding(.bottom, Design.Spacing.large) } - .padding(.horizontal, Design.Spacing.large) + .padding(.top, Design.Spacing.xLarge) + .padding(.bottom, Design.Spacing.large) } } + .accessibilityElement(children: .contain) .accessibilityLabel("Photo review") - .accessibilityHint("Use the buttons at the bottom to save or share your photo") } } diff --git a/SelfieCam/Features/Camera/Views/ShareButton.swift b/SelfieCam/Features/Camera/Views/ShareButton.swift index 2787015..5f22b8a 100644 --- a/SelfieCam/Features/Camera/Views/ShareButton.swift +++ b/SelfieCam/Features/Camera/Views/ShareButton.swift @@ -15,20 +15,15 @@ struct ShareButton: View { @State private var isShareSheetPresented = false var body: some View { - Button(action: { + Button { isShareSheetPresented = true - }) { - ZStack { - Circle() - .fill(Color.black.opacity(0.6)) - .frame(width: 80, height: 80) - - Image(systemName: "square.and.arrow.up") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - } - .shadow(radius: 5) + } label: { + Image(systemName: "square.and.arrow.up") + .font(.system(size: 20, weight: .medium)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity, maxHeight: .infinity) } + .accessibilityLabel("Share photo") .sheet(isPresented: $isShareSheetPresented) { ShareSheet(activityItems: [photo]) } diff --git a/SelfieCam/Resources/Localizable.xcstrings b/SelfieCam/Resources/Localizable.xcstrings index b136e4f..6de8ed9 100644 --- a/SelfieCam/Resources/Localizable.xcstrings +++ b/SelfieCam/Resources/Localizable.xcstrings @@ -507,6 +507,7 @@ }, "Boomerang" : { "comment" : "Display name for the \"Boomerang\" capture mode.", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "es-MX" : { @@ -769,6 +770,10 @@ } } }, + "Close" : { + "comment" : "A button label that closes the view.", + "isCommentAutoGenerated" : true + }, "Close preview" : { "comment" : "A button label that closes the preview screen.", "extractionState" : "stale", @@ -1691,6 +1696,7 @@ } }, "Photo" : { + "extractionState" : "stale", "localizations" : { "es-MX" : { "stringUnit" : { @@ -1957,6 +1963,10 @@ } } }, + "Retake photo" : { + "comment" : "A button that, when tapped, allows the user to retake a photo.", + "isCommentAutoGenerated" : true + }, "Ring Light" : { "localizations" : { "es-MX" : { @@ -2148,6 +2158,10 @@ } } }, + "Save photo" : { + "comment" : "A button that saves the currently displayed photo to the user's library.", + "isCommentAutoGenerated" : true + }, "Saved to Photos" : { "comment" : "Text shown as a toast message when a photo is successfully saved to Photos.", "extractionState" : "stale", @@ -2173,6 +2187,10 @@ } } }, + "Saves the photo to your library" : { + "comment" : "An accessibility hint for the save button in the photo review view.", + "isCommentAutoGenerated" : true + }, "Saving..." : { "comment" : "A text that appears while a photo is being saved.", "isCommentAutoGenerated" : true, @@ -2390,6 +2408,10 @@ } } }, + "Share photo" : { + "comment" : "An accessibility label for the share button.", + "isCommentAutoGenerated" : true + }, "Show colored light ring around camera preview" : { "comment" : "Subtitle for the \"Enable Ring Light\" toggle in the Settings view.", "isCommentAutoGenerated" : true, @@ -3016,6 +3038,7 @@ }, "Use the buttons at the bottom to save or share your photo" : { "comment" : "An accessibility hint for the photo review view, instructing the user on how to interact with the view.", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "es-MX" : { @@ -3065,6 +3088,7 @@ }, "Video" : { "comment" : "Display name for the \"Video\" capture mode.", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "es-MX" : {