Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
5499b10b2a
commit
fe7ab135af
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
11
README.md
11
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.
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//
|
||||
// PhotoReviewView.swift
|
||||
// CameraTester
|
||||
// SelfieCam
|
||||
//
|
||||
// Created by Matt Bruce on 1/2/26.
|
||||
//
|
||||
@ -17,57 +17,80 @@ struct PhotoReviewView: View {
|
||||
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 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)
|
||||
|
||||
// Top toolbar
|
||||
VStack {
|
||||
// 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)
|
||||
.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)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Bottom action bar
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
@ -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" : {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user