Skip to content

Commit 26cef10

Browse files
committed
feat: new features
1 parent c58b1e8 commit 26cef10

24 files changed

+4591
-141
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,12 @@ ANTHROPIC_API_KEY=
1414
# Optional settings
1515
MAX_ITERATIONS=10
1616
DEBUG=false
17+
18+
# App Store Connect API - for TestFlight uploads
19+
# Create an API key at: https://appstoreconnect.apple.com/access/integrations/api
20+
# Required for: apple/scripts/build-and-upload.sh
21+
APPLE_TEAM_ID=
22+
APP_STORE_CONNECT_KEY_ID=
23+
APP_STORE_CONNECT_ISSUER_ID=
24+
# Path to .p8 API key file (download from App Store Connect)
25+
APP_STORE_CONNECT_KEY_PATH=~/.private_keys/AuthKey_XXXXXXXXXX.p8

apple/Clarissa/Resources/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
<string>Clarissa needs to create calendar events on your behalf.</string>
6767
<key>NSPhotoLibraryUsageDescription</key>
6868
<string>Clarissa needs access to your photos to analyze images you select for text recognition, object identification, and other visual analysis.</string>
69+
<key>NSCameraUsageDescription</key>
70+
<string>Clarissa uses the camera to capture photos and scan documents for text recognition, object identification, and other visual analysis.</string>
6971

7072
<!-- URL Schemes for Deep Linking -->
7173
<key>CFBundleURLTypes</key>
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import SwiftUI
2+
import AVFoundation
3+
4+
#if os(iOS)
5+
6+
// MARK: - Camera Preview View
7+
//
8+
// SwiftUI view for displaying camera preview with capture controls.
9+
// Integrates with CameraService for photo capture and AI analysis.
10+
11+
/// SwiftUI wrapper for AVCaptureVideoPreviewLayer
12+
struct CameraPreviewView: UIViewRepresentable {
13+
let cameraService: CameraService
14+
15+
func makeUIView(context: Context) -> CameraPreviewUIView {
16+
let view = CameraPreviewUIView()
17+
view.previewLayer.session = cameraService.captureSession
18+
return view
19+
}
20+
21+
func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {
22+
// Preview layer updates automatically with session
23+
}
24+
}
25+
26+
/// UIView subclass that hosts the camera preview layer
27+
class CameraPreviewUIView: UIView {
28+
override class var layerClass: AnyClass {
29+
AVCaptureVideoPreviewLayer.self
30+
}
31+
32+
var previewLayer: AVCaptureVideoPreviewLayer {
33+
layer as! AVCaptureVideoPreviewLayer
34+
}
35+
36+
override func layoutSubviews() {
37+
super.layoutSubviews()
38+
previewLayer.frame = bounds
39+
previewLayer.videoGravity = .resizeAspectFill
40+
}
41+
}
42+
43+
// MARK: - Camera Capture View
44+
45+
/// Full camera capture interface with controls
46+
@available(iOS 26.0, *)
47+
struct CameraCaptureView: View {
48+
@StateObject private var cameraService = CameraService()
49+
@State private var capturedImage: CapturedImage?
50+
@State private var isAnalyzing = false
51+
@State private var analysisResult: String?
52+
@State private var errorMessage: String?
53+
54+
let onImageCaptured: (CapturedImage) -> Void
55+
let onDismiss: () -> Void
56+
57+
@Environment(\.accessibilityReduceMotion) private var reduceMotion
58+
59+
var body: some View {
60+
ZStack {
61+
// Camera preview
62+
if cameraService.isSessionRunning {
63+
CameraPreviewView(cameraService: cameraService)
64+
.ignoresSafeArea()
65+
} else {
66+
Color.black
67+
.ignoresSafeArea()
68+
.overlay {
69+
if cameraService.authorizationStatus == .denied {
70+
permissionDeniedView
71+
} else {
72+
ProgressView("Starting camera...")
73+
.tint(.white)
74+
}
75+
}
76+
}
77+
78+
// Controls overlay
79+
VStack {
80+
// Top bar
81+
HStack {
82+
Button {
83+
onDismiss()
84+
} label: {
85+
Image(systemName: "xmark")
86+
.font(.title2)
87+
.foregroundStyle(.white)
88+
.padding()
89+
}
90+
91+
Spacer()
92+
93+
Button {
94+
cameraService.toggleFlash()
95+
} label: {
96+
Image(systemName: cameraService.isFlashEnabled ? "bolt.fill" : "bolt.slash")
97+
.font(.title2)
98+
.foregroundStyle(cameraService.isFlashEnabled ? .yellow : .white)
99+
.padding()
100+
}
101+
}
102+
.background(.ultraThinMaterial.opacity(0.5))
103+
104+
Spacer()
105+
106+
// Bottom controls
107+
HStack(spacing: 40) {
108+
// Switch camera
109+
Button {
110+
Task {
111+
try? await cameraService.switchCamera()
112+
}
113+
} label: {
114+
Image(systemName: "arrow.triangle.2.circlepath.camera")
115+
.font(.title)
116+
.foregroundStyle(.white)
117+
}
118+
119+
// Capture button
120+
Button {
121+
Task { await captureAndAnalyze() }
122+
} label: {
123+
ZStack {
124+
Circle()
125+
.stroke(.white, lineWidth: 4)
126+
.frame(width: 70, height: 70)
127+
128+
Circle()
129+
.fill(.white)
130+
.frame(width: 58, height: 58)
131+
}
132+
}
133+
.disabled(isAnalyzing)
134+
135+
// Placeholder for symmetry
136+
Color.clear
137+
.frame(width: 44, height: 44)
138+
}
139+
.padding(.bottom, 40)
140+
.background(.ultraThinMaterial.opacity(0.5))
141+
}
142+
143+
// Analysis overlay
144+
if isAnalyzing {
145+
Color.black.opacity(0.6)
146+
.ignoresSafeArea()
147+
.overlay {
148+
VStack(spacing: 16) {
149+
ProgressView()
150+
.scaleEffect(1.5)
151+
.tint(.white)
152+
Text("Analyzing image...")
153+
.foregroundStyle(.white)
154+
}
155+
}
156+
}
157+
}
158+
.task {
159+
await setupCamera()
160+
}
161+
.alert("Error", isPresented: .constant(errorMessage != nil)) {
162+
Button("OK") { errorMessage = nil }
163+
} message: {
164+
Text(errorMessage ?? "")
165+
}
166+
}
167+
168+
private var permissionDeniedView: some View {
169+
VStack(spacing: 16) {
170+
Image(systemName: "camera.fill")
171+
.font(.system(size: 48))
172+
.foregroundStyle(.secondary)
173+
174+
Text("Camera Access Required")
175+
.font(.headline)
176+
.foregroundStyle(.white)
177+
178+
Text("Please enable camera access in Settings to use this feature.")
179+
.font(.subheadline)
180+
.foregroundStyle(.secondary)
181+
.multilineTextAlignment(.center)
182+
.padding(.horizontal)
183+
184+
Button("Open Settings") {
185+
if let url = URL(string: UIApplication.openSettingsURLString) {
186+
UIApplication.shared.open(url)
187+
}
188+
}
189+
.buttonStyle(.borderedProminent)
190+
}
191+
}
192+
193+
private func setupCamera() async {
194+
let authorized = await cameraService.requestAuthorization()
195+
if authorized {
196+
do {
197+
try await cameraService.startSession()
198+
} catch {
199+
errorMessage = error.localizedDescription
200+
}
201+
}
202+
}
203+
204+
private func captureAndAnalyze() async {
205+
isAnalyzing = true
206+
defer { isAnalyzing = false }
207+
208+
do {
209+
let image = try await cameraService.capturePhoto()
210+
capturedImage = image
211+
onImageCaptured(image)
212+
} catch {
213+
errorMessage = error.localizedDescription
214+
}
215+
}
216+
}
217+
218+
#endif
219+

0 commit comments

Comments
 (0)