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.
This commit is contained in:
parent
442f5d6e1d
commit
72bac70ea1
@ -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 */
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
// Just MCamera - nothing else - to test if it renders
|
||||
MCamera()
|
||||
.onImageCaptured { image, _ in
|
||||
print("Image captured!")
|
||||
}
|
||||
.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 {
|
||||
|
||||
@ -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.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user