diff --git a/SelfieRingLight/Features/Camera/ContentView.swift b/SelfieRingLight/Features/Camera/ContentView.swift index edec368..79e49ae 100644 --- a/SelfieRingLight/Features/Camera/ContentView.swift +++ b/SelfieRingLight/Features/Camera/ContentView.swift @@ -25,51 +25,49 @@ struct ContentView: View { let maxRingSize = calculateMaxRingSize(for: geometry) let effectiveRingSize = min(settings.ringSize, maxRingSize) - ZStack { - // MARK: - Ring Light Background - settings.lightColor - .ignoresSafeArea() - - // MARK: - Camera with MijickCamera - MCamera() - .onImageCaptured { image, _ in - handleImageCaptured(image) + // Use MCamera as the base, with ring light as border + MCamera() + .onImageCaptured { image, _ in + handleImageCaptured(image) + } + .onVideoCaptured { url, _ in + handleVideoCaptured(url) + } + .ignoresSafeArea() + .overlay { + // Ring light border overlay + ringLightOverlay(ringSize: effectiveRingSize) + } + .overlay { + // Grid overlay + if settings.isGridVisible { + GridOverlay(isVisible: true) + .padding(effectiveRingSize) + .allowsHitTesting(false) } - .onVideoCaptured { url, _ in - handleVideoCaptured(url) + } + .overlay { + // Top controls + VStack { + topControlBar + .padding(.top, effectiveRingSize + Design.Spacing.small) + Spacer() } - .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large)) - .padding(effectiveRingSize) - .animation(.easeInOut(duration: Design.Animation.quick), value: effectiveRingSize) - - // MARK: - Grid Overlay - if settings.isGridVisible { - GridOverlay(isVisible: true) - .padding(effectiveRingSize) - .allowsHitTesting(false) + .padding(.horizontal, effectiveRingSize + Design.Spacing.small) } - - // MARK: - Top Controls Overlay - VStack { - topControlBar - .padding(.top, effectiveRingSize + Design.Spacing.small) - Spacer() + .overlay { + // Toast + if let message = toastMessage { + toastView(message: message) + } } - .padding(.horizontal, effectiveRingSize + Design.Spacing.small) - - // MARK: - Toast Notification - if let message = toastMessage { - toastView(message: message) + .onChange(of: geometry.size) { _, newSize in + let newMax = min(newSize.width, newSize.height) / 4 + if settings.ringSize > newMax { + settings.ringSize = newMax + } } - } - .onChange(of: geometry.size) { _, newSize in - let newMax = min(newSize.width, newSize.height) / 4 - if settings.ringSize > newMax { - settings.ringSize = newMax - } - } } - .ignoresSafeArea() .onAppear { checkCenterStageAvailability() } @@ -90,6 +88,35 @@ struct ContentView: View { min(geometry.size.width, geometry.size.height) / 4 } + // MARK: - Ring Light Overlay + + /// Creates a ring light effect as a border around the camera + @ViewBuilder + private func ringLightOverlay(ringSize: CGFloat) -> some View { + // Use a rectangle with a large inner cutout to create the ring effect + GeometryReader { geo in + let innerRect = CGRect( + x: ringSize, + y: ringSize, + width: geo.size.width - (ringSize * 2), + height: geo.size.height - (ringSize * 2) + ) + + settings.lightColor + .mask( + Rectangle() + .overlay( + RoundedRectangle(cornerRadius: Design.CornerRadius.large) + .frame(width: innerRect.width, height: innerRect.height) + .blendMode(.destinationOut) + ) + .compositingGroup() + ) + .allowsHitTesting(false) + } + .animation(.easeInOut(duration: Design.Animation.quick), value: ringSize) + } + // MARK: - Top Control Bar private var topControlBar: some View { @@ -141,20 +168,16 @@ struct ContentView: View { // MARK: - Center Stage - /// Checks if Center Stage is available on this device private func checkCenterStageAvailability() { - // Center Stage requires a compatible device (iPad with A12+ or some iPhones) guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { isCenterStageAvailable = false return } - // Check if the device supports Center Stage isCenterStageAvailable = device.activeFormat.isCenterStageSupported isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled } - /// Toggles Center Stage on/off private func toggleCenterStage() { AVCaptureDevice.centerStageControlMode = .app AVCaptureDevice.isCenterStageEnabled.toggle() diff --git a/SelfieRingLight/Resources/Localizable.xcstrings b/SelfieRingLight/Resources/Localizable.xcstrings index 0922fc0..9e174e9 100644 --- a/SelfieRingLight/Resources/Localizable.xcstrings +++ b/SelfieRingLight/Resources/Localizable.xcstrings @@ -61,22 +61,10 @@ "comment" : "Display name for the \"Boomerang\" capture mode.", "isCommentAutoGenerated" : true }, - "Camera Access Required" : { - "comment" : "A title displayed when camera access is denied.", - "isCommentAutoGenerated" : true - }, "Cancel" : { "comment" : "The text for a button that dismisses the current view.", "isCommentAutoGenerated" : true }, - "Capture boomerang" : { - "comment" : "Label for capturing a boomerang photo.", - "isCommentAutoGenerated" : true - }, - "Capture mode: %@" : { - "comment" : "A label describing the currently selected capture mode. The placeholder is replaced with the actual mode name.", - "isCommentAutoGenerated" : true - }, "Captured boomerang" : { "comment" : "A label describing a captured boomerang.", "isCommentAutoGenerated" : true @@ -109,10 +97,6 @@ "comment" : "An accessibility label for the custom color button.", "isCommentAutoGenerated" : true }, - "Custom rotation" : { - "comment" : "Accessibility value for the rotate preview button when the user has customized the rotation angle.", - "isCommentAutoGenerated" : true - }, "Debug mode: Purchase simulated!" : { "comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.", "isCommentAutoGenerated" : true @@ -168,10 +152,6 @@ "comment" : "A hint that appears when a user taps on a color preset button.", "isCommentAutoGenerated" : true }, - "No rotation" : { - "comment" : "Accessibility value for the rotation button when the preview is not rotated.", - "isCommentAutoGenerated" : true - }, "No Watermarks • Ad-Free" : { "comment" : "Description of a benefit that comes with the Pro subscription.", "isCommentAutoGenerated" : true @@ -184,8 +164,8 @@ "comment" : "A label describing a setting that is currently enabled.", "isCommentAutoGenerated" : true }, - "Open Settings" : { - "comment" : "A button label that opens the device settings when tapped.", + "Open Source Licenses" : { + "comment" : "A heading displayed above a list of open source licenses used in the app.", "isCommentAutoGenerated" : true }, "Opens upgrade options" : { @@ -194,14 +174,6 @@ }, "Photo" : { - }, - "Photo captured" : { - "comment" : "Accessibility label for a notification that is posted when a photo is captured.", - "isCommentAutoGenerated" : true - }, - "Please enable camera access in Settings to use SelfieRingLight." : { - "comment" : "A message instructing the user to enable camera access in Settings to use SelfieRingLight.", - "isCommentAutoGenerated" : true }, "Premium color" : { "comment" : "An accessibility hint for a premium color option in the color preset button.", @@ -239,26 +211,6 @@ "comment" : "The label for the ring size slider in the settings view.", "isCommentAutoGenerated" : true }, - "Rotate preview" : { - "comment" : "A button that rotates the camera preview by 90 degrees.", - "isCommentAutoGenerated" : true - }, - "Rotated 90 degrees left" : { - "comment" : "Accessibility value for the \"Rotate preview\" button when the camera preview is rotated 90 degrees to the left.", - "isCommentAutoGenerated" : true - }, - "Rotated 90 degrees right" : { - "comment" : "Accessibility value for the \"Rotate preview\" button when the camera preview is rotated 90 degrees to the right.", - "isCommentAutoGenerated" : true - }, - "Rotated 180 degrees" : { - "comment" : "Accessibility value of the \"Rotate preview\" button when the preview is 180 degrees rotated.", - "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.", "isCommentAutoGenerated" : true @@ -306,14 +258,6 @@ "comment" : "Name of a ring light color preset.", "isCommentAutoGenerated" : true }, - "Start recording" : { - "comment" : "Label for the \"Start recording\" button in the bottom control bar when not recording a video.", - "isCommentAutoGenerated" : true - }, - "Stop recording" : { - "comment" : "Label for the button that stops recording a video.", - "isCommentAutoGenerated" : true - }, "Subscribe to %@ for %@" : { "comment" : "A button that triggers a purchase of a premium content package. The label text is generated based on the package's title and price.", "isCommentAutoGenerated" : true, @@ -326,10 +270,6 @@ } } }, - "Switch camera" : { - "comment" : "A button label that translates to \"Switch camera\".", - "isCommentAutoGenerated" : true - }, "Sync Now" : { "comment" : "A button label that triggers a sync action.", "isCommentAutoGenerated" : true @@ -349,8 +289,8 @@ "Syncing..." : { }, - "Take photo" : { - "comment" : "Label for the \"Take photo\" button in the bottom control bar when using the photo capture mode.", + "Third-party libraries used in this app" : { + "comment" : "A description of the third-party libraries used in this app.", "isCommentAutoGenerated" : true }, "Toggle grid" : { @@ -385,8 +325,8 @@ "comment" : "Display name for the \"Video\" capture mode.", "isCommentAutoGenerated" : true }, - "Video saved" : { - "comment" : "Accessibility notification text when a video is successfully saved to the user's photo library.", + "View on GitHub" : { + "comment" : "A button label that says \"View on GitHub\".", "isCommentAutoGenerated" : true }, "Warm Amber" : {