Compare commits

...

6 Commits

14 changed files with 865 additions and 1090 deletions

1195
AGENTS.md

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,8 @@
EA836ABF2F0ACE8A00077F87 /* SelfieCam.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SelfieCam.app; sourceTree = BUILT_PRODUCTS_DIR; };
EA836ACC2F0ACE8B00077F87 /* SelfieCamTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SelfieCamTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA836AD62F0ACE8B00077F87 /* SelfieCamUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SelfieCamUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EACONFIG002 /* SelfieCam/Configuration/Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SelfieCam/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; };
EACONFIG003 /* SelfieCam/Configuration/Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SelfieCam/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@ -90,6 +92,7 @@
EA836ACF2F0ACE8B00077F87 /* SelfieCamTests */,
EA836AD92F0ACE8B00077F87 /* SelfieCamUITests */,
EA836AC02F0ACE8A00077F87 /* Products */,
EADCDD7D2F12FFC6007991B3 /* Recovered References */,
);
sourceTree = "<group>";
};
@ -103,6 +106,15 @@
name = Products;
sourceTree = "<group>";
};
EADCDD7D2F12FFC6007991B3 /* Recovered References */ = {
isa = PBXGroup;
children = (
EACONFIG002 /* SelfieCam/Configuration/Debug.xcconfig */,
EACONFIG003 /* SelfieCam/Configuration/Release.xcconfig */,
);
name = "Recovered References";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -292,6 +304,7 @@
/* Begin XCBuildConfiguration section */
EA836ADE2F0ACE8B00077F87 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EACONFIG002 /* SelfieCam/Configuration/Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -325,7 +338,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -356,6 +369,7 @@
};
EA836ADF2F0ACE8B00077F87 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EACONFIG003 /* SelfieCam/Configuration/Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -389,7 +403,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -419,7 +433,7 @@
CODE_SIGN_ENTITLEMENTS = SelfieCam/SelfieCam.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos, and enable advanced features like Center Stage auto-framing.";
@ -427,8 +441,8 @@
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos to your device, making them available in the Photos app and other compatible applications.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColorName" = "LaunchBackground";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
@ -437,7 +451,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SelfieCam;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -457,7 +471,7 @@
CODE_SIGN_ENTITLEMENTS = SelfieCam/SelfieCam.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos, and enable advanced features like Center Stage auto-framing.";
@ -465,8 +479,8 @@
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos to your device, making them available in the Photos app and other compatible applications.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColorName" = "LaunchBackground";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
@ -475,7 +489,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SelfieCam;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -493,11 +507,11 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SelfieCamTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -515,11 +529,11 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SelfieCamTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -536,10 +550,10 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SelfieCamUITests;
PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -556,10 +570,10 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SelfieCamUITests;
PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;

View File

@ -7,7 +7,7 @@
<key>SelfieCam.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -0,0 +1,18 @@
// Base.xcconfig - Source of truth for all identifiers
// MIGRATION: Update COMPANY_IDENTIFIER and DEVELOPMENT_TEAM below
// =============================================================================
// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION
// =============================================================================
COMPANY_IDENTIFIER = com.mbrucedogs
APP_NAME = SelfieCam
DEVELOPMENT_TEAM = 6R7KLBPBLZ
// =============================================================================
// DERIVED IDENTIFIERS - DO NOT EDIT
// =============================================================================
APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)
TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests
UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests

View File

@ -1,6 +1,7 @@
// Debug.xcconfig
// Configuration for Debug builds
#include "Base.xcconfig"
#include? "Secrets.xcconfig"
// If Secrets.xcconfig doesn't exist (CI/CD), fall back to empty values

View File

@ -1,6 +1,7 @@
// Release.xcconfig
// Configuration for Release builds
#include "Base.xcconfig"
#include? "Secrets.xcconfig"
// If Secrets.xcconfig doesn't exist (CI/CD), fall back to empty values

View File

@ -1,71 +0,0 @@
//
// ColorPickerOverlay.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import SwiftUI
import Bedrock
// MARK: - Color Picker Overlay
struct ColorPickerOverlay: View {
@Binding var selectedColor: Color
@Binding var isPresented: Bool
private let colors: [Color] = [
.white, .red, .orange, .yellow, .green, .blue, .purple, .pink,
.gray, .black, Color(red: 1.0, green: 0.5, blue: 0.0), // Coral
Color(red: 0.5, green: 1.0, blue: 0.5), // Mint
Color(red: 0.5, green: 0.5, blue: 1.0), // Periwinkle
Color(red: 1.0, green: 0.5, blue: 1.0), // Magenta
]
var body: some View {
ZStack {
// Semi-transparent background
Color.black.opacity(Design.Opacity.medium)
.ignoresSafeArea()
// Color picker content
VStack(spacing: Design.Spacing.medium) {
// Header
Text("Ring Light Color")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(Color.white)
// Color grid
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: Design.Spacing.small) {
ForEach(colors, id: \.self) { color in
Circle()
.fill(color)
.frame(width: 50, height: 50)
.overlay(
Circle()
.stroke(Color.white.opacity(selectedColor == color ? 1.0 : 0.3), lineWidth: selectedColor == color ? 3 : 1)
)
.onTapGesture {
selectedColor = color
isPresented = false
}
}
}
// Done button
Button("Done") {
isPresented = false
}
.font(.system(size: 16, weight: .medium))
.foregroundStyle(Color.white)
.padding(.vertical, Design.Spacing.small)
}
.padding(Design.Spacing.large)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.black.opacity(Design.Opacity.strong))
)
.padding(.horizontal, Design.Spacing.large)
}
}
}

View File

@ -1,254 +0,0 @@
//
// ExpandableControlsPanel.swift
// CameraTester
//
// Created by Matt Bruce on 1/2/26.
//
import SwiftUI
import Bedrock
import MijickCamera
// MARK: - Expandable Controls Panel
struct ExpandableControlsPanel: View {
@Binding var isExpanded: Bool
// Collapsed state info
let hasActiveSettings: Bool
let activeSettingsIcons: [String]
// Control properties
let flashMode: CameraFlashMode
let flashIcon: String
let onFlashTap: () -> Void
let isFlashSyncedWithRingLight: Bool
let onFlashSyncTap: () -> Void
let hdrMode: CameraHDRMode
let hdrIcon: String
let onHDRTap: () -> Void
let isGridVisible: Bool
let gridIcon: String
let onGridTap: () -> Void
let photoQuality: PhotoQuality
let onQualityTap: () -> Void
let isCenterStageAvailable: Bool
let isCenterStageEnabled: Bool
let onCenterStageTap: () -> Void
let isFrontCamera: Bool
let onFlipCameraTap: () -> Void
let isRingLightEnabled: Bool
let onRingLightTap: () -> Void
let ringLightColor: Color
let onRingLightColorTap: () -> Void
let ringLightSize: CGFloat
let onRingLightSizeTap: () -> Void
let ringLightOpacity: Double
let onRingLightOpacityTap: () -> Void
// Layout constants
private let collapsedHeight: CGFloat = 36
private let iconSize: CGFloat = 16
private let maxVisibleIcons: Int = 3
// Grid layout
private let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
VStack(spacing: 0) {
// Header row (always visible) - acts as the collapsed pill
Button(action: { isExpanded.toggle() }) {
HStack(spacing: Design.Spacing.small) {
// Chevron that rotates
Image(systemName: "chevron.down")
.font(.system(size: iconSize, weight: .semibold))
.foregroundStyle(Color.white)
.rotationEffect(.degrees(isExpanded ? 180 : 0))
// Show active setting icons when collapsed
if !isExpanded && hasActiveSettings {
HStack(spacing: Design.Spacing.xSmall) {
ForEach(activeSettingsIcons.prefix(maxVisibleIcons), id: \.self) { icon in
Image(systemName: icon)
.font(.system(size: iconSize, weight: .medium))
.foregroundStyle(Color.yellow)
}
}
}
if isExpanded {
Spacer()
}
}
.frame(height: collapsedHeight)
.frame(maxWidth: isExpanded ? .infinity : nil)
.padding(.horizontal, Design.Spacing.medium)
}
.accessibilityLabel("Camera controls")
.accessibilityHint(isExpanded ? "Tap to collapse settings" : "Tap to expand camera settings")
// Expandable content
if isExpanded {
ScrollView {
VStack(spacing: Design.Spacing.large) {
// Camera Controls Section
VStack(spacing: Design.Spacing.medium) {
// Section header
Text("Camera Controls")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Color.white.opacity(0.8))
.frame(maxWidth: .infinity, alignment: .leading)
// Controls grid
LazyVGrid(columns: columns, spacing: Design.Spacing.medium) {
// Flash
ExpandedControlItem(
icon: flashIcon,
label: flashLabel,
isActive: flashMode != .off,
action: onFlashTap
)
// Flash Sync
ExpandedControlItem(
icon: isFlashSyncedWithRingLight ? "link" : "link.slash",
label: "SYNC",
isActive: isFlashSyncedWithRingLight,
action: onFlashSyncTap
)
// HDR
ExpandedControlItem(
icon: hdrIcon,
label: hdrLabel,
isActive: hdrMode != .off,
action: onHDRTap
)
// Grid
ExpandedControlItem(
icon: gridIcon,
label: "GRID",
isActive: isGridVisible,
action: onGridTap
)
// Quality
ExpandedControlItem(
icon: photoQuality.icon,
label: photoQuality.rawValue.uppercased(),
isActive: false,
action: onQualityTap
)
// Center Stage (if available)
if isCenterStageAvailable {
ExpandedControlItem(
icon: isCenterStageEnabled ? "person.crop.rectangle.fill" : "person.crop.rectangle",
label: "STAGE",
isActive: isCenterStageEnabled,
action: onCenterStageTap
)
}
// Flip Camera
ExpandedControlItem(
icon: "arrow.triangle.2.circlepath.camera",
label: isFrontCamera ? "FRONT" : "BACK",
isActive: false,
action: onFlipCameraTap
)
}
}
// Ring Light Section
VStack(spacing: Design.Spacing.medium) {
// Section header
Text("Ring Light")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Color.white.opacity(0.8))
.frame(maxWidth: .infinity, alignment: .leading)
// Ring light controls grid
LazyVGrid(columns: columns, spacing: Design.Spacing.medium) {
// Ring Light Enable/Disable
ExpandedControlItem(
icon: isRingLightEnabled ? "circle.fill" : "circle",
label: "ENABLE",
isActive: isRingLightEnabled,
action: onRingLightTap
)
// Ring Color
ExpandedControlItem(
icon: "circle.fill",
label: "COLOR",
isActive: false,
action: onRingLightColorTap
)
.foregroundStyle(ringLightColor)
// Ring Size
ExpandedControlItem(
icon: "circle",
label: "SIZE",
isActive: false,
action: onRingLightSizeTap
)
// Ring Brightness
ExpandedControlItem(
icon: "circle.fill",
label: "BRIGHT",
isActive: false,
action: onRingLightOpacityTap
)
.foregroundStyle(Color.white.opacity(ringLightOpacity))
}
}
}
.padding(.top, Design.Spacing.small)
.padding(.bottom, Design.Spacing.medium)
.padding(.horizontal, Design.Spacing.small)
}
.frame(maxHeight: 400) // Limit max height to prevent excessive scrolling
.scrollIndicators(.hidden) // Hide scroll indicators for cleaner look
}
}
.padding(.horizontal, Design.Spacing.small)
.background(
RoundedRectangle(cornerRadius: isExpanded ? Design.CornerRadius.large : collapsedHeight / 2)
.fill(Color.black.opacity(isExpanded ? Design.Opacity.strong : Design.Opacity.medium))
)
.animation(.easeInOut(duration: 0.25), value: isExpanded)
}
private var flashLabel: String {
switch flashMode {
case .off: return "FLASH"
case .auto: return "AUTO"
case .on: return "ON"
}
}
private var hdrLabel: String {
switch hdrMode {
case .off: return "HDR"
case .auto: return "AUTO"
case .on: return "ON"
}
}
}

View File

@ -1,45 +0,0 @@
//
// ExpandedControlItem.swift
// CameraTester
//
// Created by Matt Bruce on 1/2/26.
//
import SwiftUI
import Bedrock
// MARK: - Expanded Control Item
struct ExpandedControlItem: View {
let icon: String
let label: String
let isActive: Bool
let action: () -> Void
// Layout constants
private let iconSize: CGFloat = 28
private let buttonSize: CGFloat = 56
private let labelFontSize: CGFloat = 10
var body: some View {
Button(action: action) {
VStack(spacing: Design.Spacing.xSmall) {
ZStack {
Circle()
.fill(isActive ? Color.yellow : Color.black.opacity(Design.Opacity.light))
.frame(width: buttonSize, height: buttonSize)
Image(systemName: icon)
.font(.system(size: iconSize, weight: .medium))
.foregroundStyle(isActive ? Color.black : Color.white)
}
Text(label)
.font(.system(size: labelFontSize, weight: .medium))
.foregroundStyle(isActive ? Color.yellow : Color.white)
}
}
.accessibilityLabel("\(label)")
.accessibilityValue(isActive ? "On" : "Off")
}
}

View File

@ -1,72 +0,0 @@
//
// OpacitySliderOverlay.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import SwiftUI
import Bedrock
// MARK: - Opacity Slider Overlay
struct OpacitySliderOverlay: View {
@Binding var selectedOpacity: Double
@Binding var isPresented: Bool
private let minOpacity: Double = 0.1
private let maxOpacity: Double = 1.0
var body: some View {
ZStack {
// Semi-transparent background
Color.black.opacity(Design.Opacity.medium)
.ignoresSafeArea()
// Opacity slider content
VStack(spacing: Design.Spacing.medium) {
// Header
Text("Ring Light Brightness")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(Color.white)
// Current opacity display as percentage
Text("\(Int(selectedOpacity * 100))%")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(Color.white)
.frame(width: 80)
// Slider
Slider(value: $selectedOpacity, in: minOpacity...maxOpacity, step: 0.05)
.tint(Color.white)
.padding(.horizontal, Design.Spacing.medium)
// Opacity range labels
HStack {
Text("10%")
.font(.system(size: 14))
.foregroundStyle(Color.white.opacity(0.7))
Spacer()
Text("100%")
.font(.system(size: 14))
.foregroundStyle(Color.white.opacity(0.7))
}
.padding(.horizontal, Design.Spacing.medium)
// Done button
Button("Done") {
isPresented = false
}
.font(.system(size: 16, weight: .medium))
.foregroundStyle(Color.white)
.padding(.vertical, Design.Spacing.small)
}
.padding(Design.Spacing.large)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.black.opacity(Design.Opacity.strong))
)
.padding(.horizontal, Design.Spacing.large)
}
}
}

View File

@ -1,72 +0,0 @@
//
// SizeSliderOverlay.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import SwiftUI
import Bedrock
// MARK: - Size Slider Overlay
struct SizeSliderOverlay: View {
@Binding var selectedSize: CGFloat
@Binding var isPresented: Bool
private let minSize: CGFloat = SettingsViewModel.minRingSize
private let maxSize: CGFloat = SettingsViewModel.maxRingSize
var body: some View {
ZStack {
// Semi-transparent background
Color.black.opacity(Design.Opacity.medium)
.ignoresSafeArea()
// Size slider content
VStack(spacing: Design.Spacing.medium) {
// Header
Text("Ring Light Size")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(Color.white)
// Current size display
Text("\(Int(selectedSize))")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(Color.white)
.frame(width: 60)
// Slider
Slider(value: $selectedSize, in: minSize...maxSize, step: 5)
.tint(Color.white)
.padding(.horizontal, Design.Spacing.medium)
// Size range labels
HStack {
Text("\(Int(minSize))")
.font(.system(size: 14))
.foregroundStyle(Color.white.opacity(0.7))
Spacer()
Text("\(Int(maxSize))")
.font(.system(size: 14))
.foregroundStyle(Color.white.opacity(0.7))
}
.padding(.horizontal, Design.Spacing.medium)
// Done button
Button("Done") {
isPresented = false
}
.font(.system(size: 16, weight: .medium))
.foregroundStyle(Color.white)
.padding(.vertical, Design.Spacing.small)
}
.padding(Design.Spacing.large)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.black.opacity(Design.Opacity.strong))
)
.padding(.horizontal, Design.Spacing.large)
}
}
}

View File

@ -31,6 +31,10 @@ struct ContentView: View {
var body: some View {
ZStack {
// Background matching launch screen to prevent black flash during camera init
Color.Branding.primary
.ignoresSafeArea()
// Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes
if !showPhotoReview {
EquatableView(content: CameraContainerView(

View File

@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.450",
"green" : "0.250",
"red" : "0.850"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@ -3,6 +3,7 @@
"strings" : {
"%@" : {
"comment" : "A button with an icon and label. The argument is the text to display in the button.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -99,6 +100,7 @@
},
"%lld%%" : {
"comment" : "A text label displaying the current brightness setting of the ring light, formatted as a percentage. The argument is the current brightness setting of the ring light, as a decimal between 0.0 and 1.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -196,6 +198,7 @@
},
"10%" : {
"comment" : "A label displayed alongside the left edge of the opacity slider.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -244,6 +247,7 @@
},
"100%" : {
"comment" : "A label displayed alongside the right edge of the opacity slider.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -556,6 +560,7 @@
}
},
"Camera controls" : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -578,6 +583,7 @@
}
},
"Camera Controls" : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -769,11 +775,51 @@
},
"Choose the color of the ring light around the camera preview" : {
"comment" : "A description under the title of the light color preset section, explaining its purpose.",
"isCommentAutoGenerated" : true
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Elige el color del aro de luz alrededor de la vista previa de la cámara"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Choisissez la couleur de l'anneau lumineux autour de l'aperçu de la caméra"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Choisissez la couleur de l'anneau lumineux autour de l'aperçu de la caméra"
}
}
}
},
"Close" : {
"comment" : "A button label that closes the view.",
"isCommentAutoGenerated" : true
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cerrar"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fermer"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fermer"
}
}
}
},
"Close preview" : {
"comment" : "A button label that closes the preview screen.",
@ -1018,6 +1064,7 @@
},
"Enable Center Stage" : {
"comment" : "An accessibility label for the toggle that enables the \"Center Stage\" feature.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -1523,6 +1570,7 @@
}
},
"Last synced %@" : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -1618,6 +1666,7 @@
},
"On" : {
"comment" : "A value that describes a control item as \"On\".",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -1954,9 +2003,30 @@
},
"Retake photo" : {
"comment" : "A button that, when tapped, allows the user to retake a photo.",
"isCommentAutoGenerated" : true
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tomar otra foto"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reprendre la photo"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reprendre la photo"
}
}
}
},
"Ring Light" : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -2028,6 +2098,7 @@
},
"Ring Light Color" : {
"comment" : "The title of the color picker overlay.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2052,6 +2123,7 @@
},
"Ring Light Size" : {
"comment" : "The title of the slider that allows the user to select the size of their ring light.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2149,7 +2221,27 @@
},
"Save photo" : {
"comment" : "A button that saves the currently displayed photo to the user's library.",
"isCommentAutoGenerated" : true
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Guardar foto"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enregistrer la photo"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enregistrer la photo"
}
}
}
},
"Saved to Photos" : {
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
@ -2178,7 +2270,27 @@
},
"Saves the photo to your library" : {
"comment" : "An accessibility hint for the save button in the photo review view.",
"isCommentAutoGenerated" : true
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Guarda la foto en tu biblioteca"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enregistre la photo dans votre photothèque"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enregistre la photo dans votre photothèque"
}
}
}
},
"Saving..." : {
"comment" : "A text that appears while a photo is being saved.",
@ -2399,7 +2511,27 @@
},
"Share photo" : {
"comment" : "An accessibility label for the share button.",
"isCommentAutoGenerated" : true
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Compartir foto"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Partager la photo"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Partager la photo"
}
}
}
},
"Show colored light ring around camera preview" : {
"comment" : "Subtitle for the \"Enable Ring Light\" toggle in the Settings view.",
@ -2499,6 +2631,7 @@
},
"Sign in to iCloud to enable sync" : {
"comment" : "Subtitle of the iCloud sync section when the user is not signed into iCloud.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2599,7 +2732,7 @@
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"state" : "translated",
"value" : "Subscribe to %1$@ for %2$@"
}
},
@ -2625,6 +2758,7 @@
},
"Sync Now" : {
"comment" : "A button label that triggers a sync action.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2649,6 +2783,7 @@
},
"Sync Settings" : {
"comment" : "Title of a toggle that allows the user to enable or disable iCloud sync settings.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2673,6 +2808,7 @@
},
"Sync settings across all your devices" : {
"comment" : "Subtitle of the \"Sync Settings\" toggle in the Settings view, describing the functionality when sync is enabled.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2697,6 +2833,7 @@
},
"Synced" : {
"comment" : "Text displayed in the iCloud sync section when the user's settings have been successfully synced.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2720,6 +2857,7 @@
}
},
"Syncing..." : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -2767,6 +2905,7 @@
},
"Syncs settings across all your devices via iCloud" : {
"comment" : "An accessibility hint describing the functionality of the sync toggle in the settings view.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -2814,6 +2953,7 @@
}
},
"Tap to collapse settings" : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -2836,6 +2976,7 @@
}
},
"Tap to expand camera settings" : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -3102,6 +3243,7 @@
},
"View on GitHub" : {
"comment" : "A button label that says \"View on GitHub\".",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {