diff --git a/SelfieRingLight/Features/Camera/RingLightCameraScreen.swift b/SelfieRingLight/Features/Camera/RingLightCameraScreen.swift index 18f06b7..87fc330 100644 --- a/SelfieRingLight/Features/Camera/RingLightCameraScreen.swift +++ b/SelfieRingLight/Features/Camera/RingLightCameraScreen.swift @@ -14,31 +14,80 @@ struct RingLightCameraScreen: MCameraScreen { let isPremiumUnlocked: Bool let onSettingsTapped: () -> Void - // MARK: - Capture Button Inner Padding + // MARK: - Layout Constants + + /// Camera aspect ratio (4:3 for photos) + private let cameraAspectRatio: CGFloat = 4.0 / 3.0 + + /// Capture button inner padding private let captureButtonInnerPadding: CGFloat = 8 + /// Control bar height for layout calculations + private let controlBarHeight: CGFloat = 100 + var body: some View { - ZStack { - // Ring light background - fills entire screen - settings.lightColor - .ignoresSafeArea() + GeometryReader { geometry in + let safeArea = geometry.safeAreaInsets + let availableWidth = geometry.size.width + let availableHeight = geometry.size.height - // Camera preview with ring padding (from MijickCamera) - createCameraOutputView() - .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large)) - .padding(effectiveRingSize) + // Calculate camera size to fit within available space with ring padding + let cameraSize = calculateCameraSize( + availableWidth: availableWidth - (effectiveRingSize * 2), + availableHeight: availableHeight - (effectiveRingSize * 2) - controlBarHeight - safeArea.top + ) - // Grid overlay if enabled - GridOverlay(isVisible: settings.isGridVisible) - .padding(effectiveRingSize) - .allowsHitTesting(false) - - // Controls overlay - VStack { - topControlBar - Spacer() - bottomControlBar + ZStack { + // Ring light background - fills entire screen + settings.lightColor + .ignoresSafeArea() + + // Main content + VStack(spacing: 0) { + // Top control bar + topControlBar + .padding(.top, safeArea.top + Design.Spacing.small) + + Spacer() + + // Camera preview - centered with fixed aspect ratio + createCameraOutputView() + .frame(width: cameraSize.width, height: cameraSize.height) + .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large)) + .overlay { + // Grid overlay on top of camera + if settings.isGridVisible { + GridOverlay(isVisible: true) + .clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large)) + } + } + + Spacer() + + // Bottom control bar + bottomControlBar + .padding(.bottom, safeArea.bottom + Design.Spacing.medium) + } } + .ignoresSafeArea() + } + } + + // MARK: - Camera Size Calculation + + /// Calculates camera size maintaining aspect ratio within available space + private func calculateCameraSize(availableWidth: CGFloat, availableHeight: CGFloat) -> CGSize { + let targetWidth = availableWidth + let targetHeight = targetWidth * cameraAspectRatio + + if targetHeight <= availableHeight { + // Width-constrained + return CGSize(width: targetWidth, height: targetHeight) + } else { + // Height-constrained + let height = availableHeight + let width = height / cameraAspectRatio + return CGSize(width: width, height: height) } } @@ -51,8 +100,8 @@ struct RingLightCameraScreen: MCameraScreen { private var maxAllowedRingSize: CGFloat { let screenSize = UIScreen.main.bounds.size let smallerDimension = min(screenSize.width, screenSize.height) - // Allow ring to take up to 40% of the smaller dimension - return smallerDimension * 0.4 + // Allow ring to take up to 30% of the smaller dimension + return smallerDimension * 0.3 } // MARK: - Top Control Bar @@ -60,66 +109,79 @@ struct RingLightCameraScreen: MCameraScreen { private var topControlBar: some View { HStack { // Grid toggle - Button { + controlButton( + icon: settings.isGridVisible ? "square.grid.3x3.fill" : "square.grid.3x3", + accessibilityLabel: "Grid", + accessibilityValue: settings.isGridVisible ? "On" : "Off", + accessibilityHint: "Toggles the rule of thirds grid overlay" + ) { settings.isGridVisible.toggle() - } label: { - Image(systemName: settings.isGridVisible ? "square.grid.3x3.fill" : "square.grid.3x3") - .font(.body) - .foregroundStyle(.white) - .padding(Design.Spacing.small) - .background(.ultraThinMaterial, in: Circle()) } - .accessibilityLabel("Grid") - .accessibilityValue(settings.isGridVisible ? "On" : "Off") - .accessibilityHint("Toggles the rule of thirds grid overlay") Spacer() // Settings - Button { + controlButton( + icon: "gearshape.fill", + accessibilityLabel: "Settings", + accessibilityHint: "Opens settings" + ) { onSettingsTapped() - } label: { - Image(systemName: "gearshape.fill") - .font(.body) - .foregroundStyle(.white) - .padding(Design.Spacing.small) - .background(.ultraThinMaterial, in: Circle()) } - .accessibilityLabel("Settings") - .accessibilityHint("Opens settings") } .padding(.horizontal, Design.Spacing.large) - .padding(.top, Design.Spacing.small) } // MARK: - Bottom Control Bar private var bottomControlBar: some View { - HStack(spacing: Design.Spacing.xLarge) { - // Camera flip - Button { + HStack { + // Camera flip - left side + controlButton( + icon: "arrow.triangle.2.circlepath.camera.fill", + size: .title2, + accessibilityLabel: "Switch Camera", + accessibilityHint: "Switches between front and back camera" + ) { Task { try? await setCameraPosition(cameraPosition == .front ? .back : .front) } - } label: { - Image(systemName: "arrow.triangle.2.circlepath.camera.fill") - .font(.title2) - .foregroundStyle(.white) - .padding(Design.Spacing.medium) - .background(.ultraThinMaterial, in: Circle()) } - .accessibilityLabel("Switch Camera") - .accessibilityHint("Switches between front and back camera") - // Capture button + Spacer() + + // Capture button - center captureButton - // Placeholder for symmetry + Spacer() + + // Placeholder for symmetry - right side Color.clear .frame(width: 44, height: 44) } .padding(.horizontal, Design.Spacing.xLarge) - .padding(.bottom, Design.Spacing.large) + } + + // MARK: - Control Button Helper + + private func controlButton( + icon: String, + size: Font = .body, + accessibilityLabel: String, + accessibilityValue: String? = nil, + accessibilityHint: String? = nil, + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + Image(systemName: icon) + .font(size) + .foregroundStyle(.white) + .padding(Design.Spacing.small) + .background(.ultraThinMaterial, in: Circle()) + } + .accessibilityLabel(accessibilityLabel) + .accessibilityValue(accessibilityValue ?? "") + .accessibilityHint(accessibilityHint ?? "") } // MARK: - Capture Button @@ -129,14 +191,17 @@ struct RingLightCameraScreen: MCameraScreen { captureOutput() } label: { ZStack { + // Outer white ring Circle() .fill(.white) .frame(width: Design.Capture.buttonSize, height: Design.Capture.buttonSize) + // Colored border matching ring light Circle() .strokeBorder(settings.lightColor, lineWidth: Design.LineWidth.thick) .frame(width: Design.Capture.buttonSize, height: Design.Capture.buttonSize) + // Inner white circle Circle() .fill(.white) .frame( diff --git a/SelfieRingLight/Resources/Localizable.xcstrings b/SelfieRingLight/Resources/Localizable.xcstrings index 688fdd8..31fd8e6 100644 --- a/SelfieRingLight/Resources/Localizable.xcstrings +++ b/SelfieRingLight/Resources/Localizable.xcstrings @@ -5,10 +5,6 @@ "comment" : "The value of the ring size slider, displayed in parentheses.", "isCommentAutoGenerated" : true }, - "%lld%%" : { - "comment" : "A text label displaying the current brightness percentage.", - "isCommentAutoGenerated" : true - }, "%lldpt" : { "comment" : "A label displaying the current ring size, formatted as a number followed by the unit \"pt\".", "isCommentAutoGenerated" : true @@ -65,8 +61,8 @@ "comment" : "The text for a button that dismisses the current view.", "isCommentAutoGenerated" : true }, - "Captured boomerang" : { - "comment" : "A label describing a captured boomerang.", + "Capture" : { + "comment" : "A button that, when tapped, takes a photo.```", "isCommentAutoGenerated" : true }, "Captured photo" : { @@ -109,10 +105,6 @@ "comment" : "The text for a button that dismisses a view. In this case, it dismisses the settings view.", "isCommentAutoGenerated" : true }, - "Edit" : { - "comment" : "Label for the button that allows the user to edit their captured photo or video.", - "isCommentAutoGenerated" : true - }, "Front Flash" : { "comment" : "Title of a toggle in the Settings view that controls whether the front flash is enabled.", "isCommentAutoGenerated" : true @@ -121,6 +113,10 @@ "comment" : "The title of the \"Go Pro\" button in the Pro paywall.", "isCommentAutoGenerated" : true }, + "Grid" : { + "comment" : "A button that toggles the visibility of a grid overlay in the camera view.", + "isCommentAutoGenerated" : true + }, "Grid Overlay" : { "comment" : "Text displayed in a settings toggle for showing a grid overlay to help compose your shot.", "isCommentAutoGenerated" : true @@ -152,20 +148,24 @@ "comment" : "The accessibility value for the grid toggle when it is off.", "isCommentAutoGenerated" : true }, + "On" : { + "comment" : "A label that describes a setting as \"On\".", + "isCommentAutoGenerated" : true + }, "Open Source Licenses" : { "comment" : "A heading displayed above a list of open source licenses used in the app.", "isCommentAutoGenerated" : true }, + "Opens settings" : { + "comment" : "A hint describing the action of tapping the settings button.", + "isCommentAutoGenerated" : true + }, "Opens upgrade options" : { "comment" : "An accessibility hint for the \"Upgrade to Pro\" button that indicates it opens upgrade options.", "isCommentAutoGenerated" : true }, "Photo" : { - }, - "Photo captured" : { - "comment" : "Voiceover announcement when a photo is captured.", - "isCommentAutoGenerated" : true }, "Premium color" : { "comment" : "An accessibility hint for a premium color option in the color preset button.", @@ -191,10 +191,6 @@ "comment" : "Title for a button that allows the user to retake a captured photo or video.", "isCommentAutoGenerated" : true }, - "Ring Glow" : { - "comment" : "Title of a slider that controls the intensity of the ring glow effect in the captured media.", - "isCommentAutoGenerated" : true - }, "Ring size" : { "comment" : "An accessibility label for the ring size slider in the settings view.", "isCommentAutoGenerated" : true @@ -203,6 +199,10 @@ "comment" : "The label for the ring size slider in the settings view.", "isCommentAutoGenerated" : true }, + "Save" : { + "comment" : "Title for a button that saves the currently captured photo or video to the user's photo library.", + "isCommentAutoGenerated" : true + }, "Saved to Photos" : { "comment" : "Text shown as a toast message when a photo is successfully saved to Photos.", "isCommentAutoGenerated" : true @@ -242,9 +242,6 @@ "Skin Smoothing" : { "comment" : "A toggle that enables or disables real-time skin smoothing.", "isCommentAutoGenerated" : true - }, - "Smoothing" : { - }, "Soft Pink" : { "comment" : "Name of a ring light color preset.", @@ -262,6 +259,14 @@ } } }, + "Switch Camera" : { + "comment" : "A button that switches between the front and back camera.", + "isCommentAutoGenerated" : true + }, + "Switches between front and back camera" : { + "comment" : "A hint that describes the functionality of the \"Switch Camera\" button in the camera screen.", + "isCommentAutoGenerated" : true + }, "Sync Now" : { "comment" : "A button label that triggers a sync action.", "isCommentAutoGenerated" : true @@ -280,11 +285,19 @@ }, "Syncing..." : { + }, + "Takes a photo" : { + "comment" : "An accessibility hint for the capture button.", + "isCommentAutoGenerated" : true }, "Third-party libraries used in this app" : { "comment" : "A description of the third-party libraries used in this app.", "isCommentAutoGenerated" : true }, + "Toggles the rule of thirds grid overlay" : { + "comment" : "An accessibility hint for the grid toggle button in the top control bar of the ring light camera screen.", + "isCommentAutoGenerated" : true + }, "True Mirror" : { "comment" : "Title of a toggle in the settings view that allows the user to flip the camera preview.", "isCommentAutoGenerated" : true @@ -293,10 +306,6 @@ "comment" : "Description of a benefit that comes with the Pro subscription, specifically related to the boomerang tool.", "isCommentAutoGenerated" : true }, - "Unlock filters, AI remove, and more with Pro" : { - "comment" : "A teaser text that appears below the capture edit view, promoting a premium feature.", - "isCommentAutoGenerated" : true - }, "Unlock premium colors, video, and more" : { "comment" : "A description of the benefits of upgrading to the Pro version of the app.", "isCommentAutoGenerated" : true @@ -313,10 +322,6 @@ "comment" : "Display name for the \"Video\" capture mode.", "isCommentAutoGenerated" : true }, - "Video saved" : { - "comment" : "Voiceover text announced when a video is successfully saved to the user's Photos library.", - "isCommentAutoGenerated" : true - }, "View on GitHub" : { "comment" : "A button label that says \"View on GitHub\".", "isCommentAutoGenerated" : true