// // PhotoPickerView.swift // Reflect // // Photo picker and gallery for mood entry attachments. // import SwiftUI import PhotosUI // MARK: - Photo Picker View struct PhotoPickerView: View { @Environment(\.dismiss) private var dismiss let entry: MoodEntryModel let onPhotoSelected: (UIImage) -> Void @State private var selectedItem: PhotosPickerItem? @State private var showCamera = false @State private var isProcessing = false var body: some View { NavigationStack { VStack(spacing: 24) { // Header VStack(spacing: 8) { Image(systemName: "photo.on.rectangle.angled") .font(.largeTitle) .foregroundStyle(.secondary) Text("Add a Photo") .font(.title2) .fontWeight(.semibold) Text("Capture how you're feeling today") .font(.subheadline) .foregroundStyle(.secondary) } .padding(.top, 40) Spacer() // Options VStack(spacing: 16) { // Photo Library PhotosPicker(selection: $selectedItem, matching: .images) { HStack(spacing: 16) { Image(systemName: "photo.stack") .font(.title2) .frame(width: 44, height: 44) .background(Color.blue.opacity(0.15)) .foregroundColor(.blue) .clipShape(Circle()) VStack(alignment: .leading, spacing: 2) { Text("Photo Library") .font(.headline) .foregroundColor(.primary) Text("Choose from your photos") .font(.subheadline) .foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.right") .foregroundStyle(.tertiary) } .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(Color(.systemBackground)) ) } .disabled(isProcessing) .accessibilityIdentifier(AccessibilityID.PhotoPicker.photosPicker) // Camera Button { showCamera = true } label: { HStack(spacing: 16) { Image(systemName: "camera") .font(.title2) .frame(width: 44, height: 44) .background(Color.green.opacity(0.15)) .foregroundColor(.green) .clipShape(Circle()) VStack(alignment: .leading, spacing: 2) { Text("Take Photo") .font(.headline) .foregroundColor(.primary) Text("Use your camera") .font(.subheadline) .foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.right") .foregroundStyle(.tertiary) } .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(Color(.systemBackground)) ) } .disabled(isProcessing) .accessibilityIdentifier(AccessibilityID.PhotoPicker.cameraButton) } .padding(.horizontal) Spacer() // Loading indicator if isProcessing { ProgressView("Processing...") .padding() } } .background(Color(.systemGroupedBackground)) .navigationTitle("Add Photo") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } .accessibilityIdentifier(AccessibilityID.PhotoPicker.cancelButton) } } .onChange(of: selectedItem) { _, newItem in Task { await loadImage(from: newItem) } } .fullScreenCover(isPresented: $showCamera) { CameraView { image in handleSelectedImage(image) } } } } private func loadImage(from item: PhotosPickerItem?) async { guard let item = item else { return } isProcessing = true defer { isProcessing = false } do { if let data = try await item.loadTransferable(type: Data.self), let image = UIImage(data: data) { handleSelectedImage(image) } } catch { #if DEBUG print("PhotoPickerView: Failed to load image: \(error)") #endif } } private func handleSelectedImage(_ image: UIImage) { onPhotoSelected(image) dismiss() } } // MARK: - Camera View struct CameraView: UIViewControllerRepresentable { @Environment(\.dismiss) private var dismiss let onImageCaptured: (UIImage) -> Void func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = .camera picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { let parent: CameraView init(_ parent: CameraView) { self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let image = info[.originalImage] as? UIImage { parent.onImageCaptured(image) } parent.dismiss() } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { parent.dismiss() } } } // MARK: - Photo Gallery View (Full screen photo viewer) struct PhotoGalleryView: View { @Environment(\.dismiss) private var dismiss let photoID: UUID let onDelete: () -> Void @State private var showDeleteConfirmation = false @State private var scale: CGFloat = 1.0 @State private var lastScale: CGFloat = 1.0 @State private var offset: CGSize = .zero @State private var lastOffset: CGSize = .zero var body: some View { NavigationStack { GeometryReader { geometry in ZStack { Color.black.ignoresSafeArea() if let image = PhotoManager.shared.image(for: photoID) { image .resizable() .aspectRatio(contentMode: .fit) .scaleEffect(scale) .offset(offset) .gesture( MagnificationGesture() .onChanged { value in scale = lastScale * value } .onEnded { _ in lastScale = scale if scale < 1 { withAnimation { scale = 1 lastScale = 1 offset = .zero lastOffset = .zero } } } ) .simultaneousGesture( DragGesture() .onChanged { value in if scale > 1 { offset = CGSize( width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height ) } } .onEnded { _ in lastOffset = offset } ) .onTapGesture(count: 2) { withAnimation { if scale > 1 { scale = 1 lastScale = 1 offset = .zero lastOffset = .zero } else { scale = 2 lastScale = 2 } } } .accessibilityIdentifier(AccessibilityID.PhotoPicker.photoImage) } else { VStack(spacing: 16) { Image(systemName: "photo.badge.exclamationmark") .font(.largeTitle) .foregroundColor(.gray) Text("Photo not found") .foregroundColor(.gray) } } } } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.hidden, for: .navigationBar) .toolbar { ToolbarItem(placement: .cancellationAction) { Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") .font(.title2) .foregroundStyle(.white.opacity(0.7)) } .accessibilityIdentifier(AccessibilityID.PhotoPicker.closeButton) } ToolbarItem(placement: .primaryAction) { Menu { Button { sharePhoto() } label: { Label("Share", systemImage: "square.and.arrow.up") } .accessibilityIdentifier(AccessibilityID.PhotoPicker.shareButton) Button(role: .destructive) { showDeleteConfirmation = true } label: { Label("Delete", systemImage: "trash") } .accessibilityIdentifier(AccessibilityID.PhotoPicker.deleteButton) } label: { Image(systemName: "ellipsis.circle.fill") .font(.title2) .foregroundStyle(.white.opacity(0.7)) } .accessibilityIdentifier(AccessibilityID.PhotoPicker.menuButton) } } .confirmationDialog("Delete Photo", isPresented: $showDeleteConfirmation, titleVisibility: .visible) { Button("Delete", role: .destructive) { onDelete() dismiss() } .accessibilityIdentifier(AccessibilityID.PhotoPicker.deleteConfirmButton) Button("Cancel", role: .cancel) { } .accessibilityIdentifier(AccessibilityID.PhotoPicker.deleteCancelButton) } message: { Text("Are you sure you want to delete this photo?") } } } private func sharePhoto() { guard let uiImage = PhotoManager.shared.loadPhoto(id: photoID) else { return } let activityVC = UIActivityViewController( activityItems: [uiImage], applicationActivities: nil ) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootVC = windowScene.windows.first?.rootViewController { rootVC.present(activityVC, animated: true) } } } // MARK: - Photo Thumbnail View (for list display) struct PhotoThumbnailView: View { let photoID: UUID? let size: CGFloat var body: some View { if let photoID = photoID, let image = PhotoManager.shared.thumbnail(for: photoID) { image .resizable() .aspectRatio(contentMode: .fill) .frame(width: size, height: size) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { RoundedRectangle(cornerRadius: 8) .fill(Color(.systemGray5)) .frame(width: size, height: size) .overlay( Image(systemName: "photo") .foregroundStyle(.tertiary) ) } } }