From 72bac70ea1fe834e39564d129511649e4570a7c5 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 2 Jan 2026 16:32:09 -0600 Subject: [PATCH] Test: Minimal MCamera implementation to verify it works Simple ContentView with just MCamera() and nothing else. If this works, we can add the ring light back incrementally. --- SelfieRingLight.xcodeproj/project.pbxproj | 12 + .../Features/Camera/ContentView.swift | 428 +----------------- .../Resources/Localizable.xcstrings | 6 +- 3 files changed, 22 insertions(+), 424 deletions(-) diff --git a/SelfieRingLight.xcodeproj/project.pbxproj b/SelfieRingLight.xcodeproj/project.pbxproj index e4714f7..9dab21d 100644 --- a/SelfieRingLight.xcodeproj/project.pbxproj +++ b/SelfieRingLight.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C852F08306200DC03E1 /* RevenueCat */; }; EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C872F08306200DC03E1 /* RevenueCatUI */; }; EA766F022F08500000DC03E1 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA766F012F08500000DC03E1 /* Bedrock */; }; + EA766F102F08600000DC03E1 /* MijickCamera in Frameworks */ = {isa = PBXBuildFile; productRef = EA766F0F2F08600000DC03E1 /* MijickCamera */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -63,6 +64,7 @@ EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */, EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */, EA766F022F08500000DC03E1 /* Bedrock in Frameworks */, + EA766F102F08600000DC03E1 /* MijickCamera in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -136,6 +138,7 @@ EA766C852F08306200DC03E1 /* RevenueCat */, EA766C872F08306200DC03E1 /* RevenueCatUI */, EA766F012F08500000DC03E1 /* Bedrock */, + EA766F0F2F08600000DC03E1 /* MijickCamera */, ); productName = SelfieRingLight; productReference = EA766C2C2F082A8400DC03E1 /* SelfieRingLight.app */; @@ -222,6 +225,7 @@ packageReferences = ( EA766C822F08306200DC03E1 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */, EA766F002F08500000DC03E1 /* XCRemoteSwiftPackageReference "Bedrock" */, + EA766F0E2F08600000DC03E1 /* XCRemoteSwiftPackageReference "Camera" */, ); preferredProjectObjectVersion = 77; productRefGroup = EA766C2D2F082A8400DC03E1 /* Products */; @@ -638,6 +642,14 @@ kind = branch; }; }; + EA766F0E2F08600000DC03E1 /* XCRemoteSwiftPackageReference "Camera" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Mijick/Camera"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ diff --git a/SelfieRingLight/Features/Camera/ContentView.swift b/SelfieRingLight/Features/Camera/ContentView.swift index edf49a9..acb4993 100644 --- a/SelfieRingLight/Features/Camera/ContentView.swift +++ b/SelfieRingLight/Features/Camera/ContentView.swift @@ -1,433 +1,17 @@ import SwiftUI +import MijickCamera import Bedrock +/// Simple test view to verify MijickCamera works struct ContentView: View { - @State private var viewModel = CameraViewModel() - @State private var premiumManager = PremiumManager() - @State private var showPaywall = false - @State private var showSettings = false - @State private var showShareSheet = false - var body: some View { - GeometryReader { geometry in - let maxRingSize = calculateMaxRingSize(for: geometry) - let effectiveRingSize = min(viewModel.settings.ringSize, maxRingSize) - - ZStack { - // MARK: - Ring Light Background - ringLightBackground - - // MARK: - Camera Preview (full screen with ring border) - cameraPreviewArea(ringSize: effectiveRingSize) - - // MARK: - Grid Overlay - if viewModel.settings.isGridVisible && !viewModel.isPreviewHidden { - GridOverlay(isVisible: true) - .padding(effectiveRingSize) - } - - // MARK: - Controls Overlay (on top of preview) - controlsOverlay(ringSize: effectiveRingSize) - - // MARK: - Permission Denied View - if !viewModel.isCameraAuthorized && viewModel.captureSession != nil { - permissionDeniedView - } - - // MARK: - Toast Notification - if let message = viewModel.toastMessage { - toastView(message: message) - } + // Just MCamera - nothing else - to test if it renders + MCamera() + .onImageCaptured { image, _ in + print("Image captured!") } - .onChange(of: geometry.size) { _, newSize in - // Update max ring size when screen size changes - let newMax = min(newSize.width, newSize.height) / 4 - if viewModel.settings.ringSize > newMax { - viewModel.settings.ringSize = newMax - } - } - } - .ignoresSafeArea() - .task { - await viewModel.setupCamera() - } - .onDisappear { - viewModel.restoreBrightness() - } - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - viewModel.updateVideoOrientation(for: UIDevice.current.orientation) - } - .sheet(isPresented: $showPaywall) { - ProPaywallView() - } - .sheet(isPresented: $showSettings) { - SettingsView(viewModel: viewModel.settings, showPaywall: $showPaywall) - } - .fullScreenCover(isPresented: $viewModel.showPostCapturePreview) { - if let media = viewModel.capturedMedia { - PostCapturePreviewView( - media: media, - isPremiumUnlocked: premiumManager.isPremiumUnlocked, - onRetake: { - viewModel.retakeCapture() - }, - onSave: { - viewModel.saveCurrentCapture() - }, - onShare: { - showShareSheet = true - }, - onDismiss: { - viewModel.dismissPostCapturePreview() - } - ) - .sheet(isPresented: $showShareSheet) { - ShareSheet(items: viewModel.getShareItems()) - } - } - } - } - - // MARK: - Max Ring Size Calculation - - /// Calculates maximum ring size based on screen dimensions - /// Ring should not exceed 1/4 of the smaller dimension - private func calculateMaxRingSize(for geometry: GeometryProxy) -> CGFloat { - min(geometry.size.width, geometry.size.height) / 4 - } - - // MARK: - Ring Light Background - - @ViewBuilder - private var ringLightBackground: some View { - // Always use the selected light color - premium checks are done in Settings - viewModel.settings.lightColor .ignoresSafeArea() } - - // MARK: - Camera Preview Area - - @ViewBuilder - private func cameraPreviewArea(ringSize: CGFloat) -> some View { - if viewModel.isCameraAuthorized { - // Show preview unless front flash is active - if !viewModel.isPreviewHidden { - CameraPreview( - viewModel: viewModel, - isMirrorFlipped: viewModel.settings.isMirrorFlipped, - zoomFactor: viewModel.settings.currentZoomFactor, - manualRotationAngle: viewModel.manualRotationAngle, - ringLightColor: viewModel.settings.lightColor - ) - .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) - .padding(ringSize) - .animation(.easeInOut(duration: Design.Animation.quick), value: ringSize) - .animation(.easeInOut(duration: Design.Animation.quick), value: viewModel.manualRotationAngle) - } - } else { - // Show placeholder while requesting permission - Rectangle() - .fill(viewModel.settings.lightColor) - .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) - .padding(ringSize) - .animation(.easeInOut(duration: Design.Animation.quick), value: ringSize) - .overlay { - if viewModel.captureSession == nil { - ProgressView() - .tint(.white) - .scaleEffect(1.5) - } - } - } - } - - // MARK: - Controls Overlay - - private func controlsOverlay(ringSize: CGFloat) -> some View { - VStack { - // Top bar - topControlBar - .padding(.top, ringSize + Design.Spacing.small) - - Spacer() - - // Bottom capture controls - bottomControlBar - .padding(.bottom, ringSize + Design.Spacing.medium) - } - .padding(.horizontal, ringSize + Design.Spacing.small) - .animation(.easeInOut(duration: Design.Animation.quick), value: ringSize) - } - - // MARK: - Top Control Bar - - private var topControlBar: some View { - HStack { - // Center Stage button (only shown when available) - if viewModel.isCenterStageAvailable { - Button { - viewModel.toggleCenterStage() - } label: { - Image(systemName: viewModel.isCenterStageEnabled ? "person.crop.rectangle.fill" : "person.crop.rectangle") - .font(.body) - .foregroundStyle(viewModel.isCenterStageEnabled ? .yellow : .white) - .padding(Design.Spacing.small) - .background(.ultraThinMaterial, in: .circle) - } - .accessibilityLabel(String(localized: "Center Stage")) - .accessibilityValue(viewModel.isCenterStageEnabled ? "On" : "Off") - .accessibilityHint(String(localized: "Keeps you centered in frame")) - } - - // Rotate preview button - Button { - withAnimation(.easeInOut(duration: Design.Animation.quick)) { - viewModel.cycleManualRotation() - } - } label: { - Image(systemName: rotationIconName) - .font(.body) - .foregroundStyle(viewModel.manualRotationAngle != 0 ? .yellow : .white) - .padding(Design.Spacing.small) - .background(.ultraThinMaterial, in: .circle) - } - .accessibilityLabel(String(localized: "Rotate preview")) - .accessibilityValue(rotationAccessibilityValue) - .accessibilityHint(String(localized: "Rotates the camera preview 90 degrees")) - - Spacer() - - // Grid toggle - Button { - viewModel.settings.isGridVisible.toggle() - } label: { - Image(systemName: "square.grid.3x3") - .font(.body) - .foregroundStyle(viewModel.settings.isGridVisible ? .yellow : .white) - .padding(Design.Spacing.small) - .background(.ultraThinMaterial, in: .circle) - } - .accessibilityLabel(String(localized: "Toggle grid")) - .accessibilityValue(viewModel.settings.isGridVisible ? "On" : "Off") - - // Settings button - Button { - showSettings = true - } label: { - Image(systemName: "gearshape.fill") - .font(.body) - .foregroundStyle(.white) - .padding(Design.Spacing.small) - .background(.ultraThinMaterial, in: .circle) - } - .accessibilityLabel(String(localized: "Settings")) - } - } - - /// Icon name for rotation button based on current rotation - private var rotationIconName: String { - switch viewModel.manualRotationAngle { - case 90: - return "rotate.right.fill" - case 180: - return "arrow.up.arrow.down" - case 270: - return "rotate.left.fill" - default: - return "rotate.right" - } - } - - /// Accessibility value for rotation button - private var rotationAccessibilityValue: String { - switch viewModel.manualRotationAngle { - case 0: - return String(localized: "No rotation") - case 90: - return String(localized: "Rotated 90 degrees right") - case 180: - return String(localized: "Rotated 180 degrees") - case 270: - return String(localized: "Rotated 90 degrees left") - default: - return String(localized: "Custom rotation") - } - } - - // MARK: - Bottom Control Bar - - private var bottomControlBar: some View { - HStack { - // Switch camera button - Button { - viewModel.switchCamera() - } label: { - Image(systemName: "camera.rotate.fill") - .font(.title2) - .foregroundStyle(.white) - .padding(Design.Spacing.medium) - .background(.ultraThinMaterial, in: .circle) - } - .accessibilityLabel(String(localized: "Switch camera")) - - Spacer() - - // Capture button - captureButton - - Spacer() - - // Capture mode selector - captureModeMenu - } - } - - // MARK: - Capture Button - - private var captureButton: some View { - Button { - captureAction() - } label: { - ZStack { - Circle() - .fill(.white) - .frame(width: Design.Capture.buttonSize, height: Design.Capture.buttonSize) - - Circle() - .stroke(.white, lineWidth: Design.LineWidth.thick) - .frame(width: Design.Capture.buttonSize + Design.Spacing.small, height: Design.Capture.buttonSize + Design.Spacing.small) - - // Show red stop square when recording - if viewModel.isRecording { - RoundedRectangle(cornerRadius: Design.CornerRadius.xSmall) - .fill(.red) - .frame(width: Design.Capture.stopSquare, height: Design.Capture.stopSquare) - } - } - } - .accessibilityLabel(captureButtonLabel) - .disabled(!viewModel.canCapture) - } - - // MARK: - Capture Mode Menu - - private var captureModeMenu: some View { - Menu { - ForEach(CaptureMode.allCases) { mode in - Button { - if !mode.isPremium || premiumManager.isPremiumUnlocked { - viewModel.settings.selectedCaptureMode = mode - } else { - showPaywall = true - } - } label: { - HStack { - Label(mode.displayName, systemImage: mode.systemImage) - if mode.isPremium && !premiumManager.isPremiumUnlocked { - Image(systemName: "crown.fill") - } - } - } - } - } label: { - Image(systemName: viewModel.settings.selectedCaptureMode.systemImage) - .font(.title2) - .foregroundStyle(.white) - .padding(Design.Spacing.medium) - .background(.ultraThinMaterial, in: .circle) - } - .accessibilityLabel(String(localized: "Capture mode: \(viewModel.settings.selectedCaptureMode.displayName)")) - } - - // MARK: - Permission Denied View - - private var permissionDeniedView: some View { - VStack(spacing: Design.Spacing.large) { - Image(systemName: "camera.fill") - .font(.system(size: Design.BaseFontSize.hero)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Text("Camera Access Required") - .font(.title2.bold()) - .foregroundStyle(.white) - - Text("Please enable camera access in Settings to use SelfieRingLight.") - .font(.body) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - .multilineTextAlignment(.center) - .padding(.horizontal, Design.Spacing.xLarge) - - Button("Open Settings") { - if let url = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(url) - } - } - .buttonStyle(.borderedProminent) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(.black.opacity(Design.Opacity.heavy)) - } - - // MARK: - Capture Action - - private func captureAction() { - switch viewModel.settings.selectedCaptureMode { - case .photo: - viewModel.capturePhoto() - case .video: - if viewModel.isRecording { - viewModel.stopRecording() - } else { - viewModel.startRecording() - } - case .boomerang: - // TODO: Implement boomerang capture - viewModel.capturePhoto() - } - } - - private var captureButtonLabel: String { - switch viewModel.settings.selectedCaptureMode { - case .photo: - return String(localized: "Take photo") - case .video: - return viewModel.isRecording ? String(localized: "Stop recording") : String(localized: "Start recording") - case .boomerang: - return String(localized: "Capture boomerang") - } - } - - // MARK: - Toast View - - private func toastView(message: String) -> some View { - VStack { - Spacer() - - Text(message) - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.white) - .padding(.horizontal, Design.Spacing.large) - .padding(.vertical, Design.Spacing.medium) - .background(.ultraThinMaterial, in: Capsule()) - .padding(.bottom, Design.Spacing.xxxLarge) - .transition(.move(edge: .bottom).combined(with: .opacity)) - .animation(.spring(duration: Design.Animation.quick), value: message) - } - .accessibilityLabel(message) - } -} - -// MARK: - Share Sheet - -/// UIKit wrapper for UIActivityViewController -struct ShareSheet: UIViewControllerRepresentable { - let items: [Any] - - func makeUIViewController(context: Context) -> UIActivityViewController { - UIActivityViewController(activityItems: items, applicationActivities: nil) - } - - func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} } #Preview { diff --git a/SelfieRingLight/Resources/Localizable.xcstrings b/SelfieRingLight/Resources/Localizable.xcstrings index f9d5868..8016193 100644 --- a/SelfieRingLight/Resources/Localizable.xcstrings +++ b/SelfieRingLight/Resources/Localizable.xcstrings @@ -244,7 +244,8 @@ "isCommentAutoGenerated" : true }, "Rotate preview" : { - + "comment" : "A button that rotates the camera preview by 90 degrees.", + "isCommentAutoGenerated" : true }, "Rotated 90 degrees left" : { "comment" : "An accessibility label describing a 90-degree left rotation.", @@ -259,7 +260,8 @@ "isCommentAutoGenerated" : true }, "Rotates the camera preview 90 degrees" : { - + "comment" : "A hint that explains how to rotate the camera preview.", + "isCommentAutoGenerated" : true }, "Saved to Photos" : { "comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",