Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
499889d373
commit
8f01d78146
1
PRD.md
1
PRD.md
@ -44,6 +44,7 @@ SelfieCam is a professional-grade selfie camera app featuring a customizable scr
|
|||||||
1. **Protocol-Oriented Programming (POP)** - All shared capabilities defined via protocols before concrete types
|
1. **Protocol-Oriented Programming (POP)** - All shared capabilities defined via protocols before concrete types
|
||||||
2. **MVVM-lite** - Views are "dumb" renderers; all logic lives in `@Observable` view models
|
2. **MVVM-lite** - Views are "dumb" renderers; all logic lives in `@Observable` view models
|
||||||
3. **Bedrock Design System** - Centralized design tokens, no magic numbers
|
3. **Bedrock Design System** - Centralized design tokens, no magic numbers
|
||||||
|
- Settings layout contract: `SettingsCard` owns horizontal insets, custom rows use `SettingsCardRow`, and in-card separators use `SettingsDivider`
|
||||||
4. **Full Accessibility** - Dynamic Type, VoiceOver labels/hints/traits/announcements
|
4. **Full Accessibility** - Dynamic Type, VoiceOver labels/hints/traits/announcements
|
||||||
5. **Modern Swift & SwiftUI** - Swift 6 concurrency, `@MainActor`, modern APIs
|
5. **Modern Swift & SwiftUI** - Swift 6 concurrency, `@MainActor`, modern APIs
|
||||||
6. **Testable & Reusable Design** - Protocols enable mocking and future package extraction
|
6. **Testable & Reusable Design** - Protocols enable mocking and future package extraction
|
||||||
|
|||||||
@ -53,6 +53,7 @@ Perfect for low-light selfies, content creation, video calls, makeup application
|
|||||||
- Dynamic Type and ScaledMetric for readable text at all sizes
|
- Dynamic Type and ScaledMetric for readable text at all sizes
|
||||||
- String Catalog localization ready (`.xcstrings`)
|
- String Catalog localization ready (`.xcstrings`)
|
||||||
- Consistent design system using Bedrock framework
|
- Consistent design system using Bedrock framework
|
||||||
|
- Settings cards follow Bedrock’s row contract (`SettingsCard` + `SettingsCardRow` + `SettingsDivider`) for consistent insets and row rhythm
|
||||||
- Prevents screen dimming during camera use
|
- Prevents screen dimming during camera use
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|||||||
@ -442,7 +442,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(PRODUCT_NAME)";
|
PRODUCT_NAME = "$(PRODUCT_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@ -472,7 +472,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(PRODUCT_NAME)";
|
PRODUCT_NAME = "$(PRODUCT_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
|||||||
Binary file not shown.
@ -39,6 +39,7 @@ struct OnboardingSettingsView: View {
|
|||||||
isOn: $viewModel.isRingLightEnabled,
|
isOn: $viewModel.isRingLightEnabled,
|
||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Camera Position using Bedrock SettingsSegmentedPicker
|
// Camera Position using Bedrock SettingsSegmentedPicker
|
||||||
SettingsSegmentedPicker(
|
SettingsSegmentedPicker(
|
||||||
|
|||||||
@ -43,12 +43,17 @@ struct SettingsView: View {
|
|||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
.accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
|
.accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Ring Size Slider
|
// Ring Size Slider
|
||||||
ringSizeSlider
|
ringSizeSlider
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Color Preset
|
// Color Preset
|
||||||
colorPresetSection
|
SettingsCardRow {
|
||||||
|
colorPresetSection
|
||||||
|
}
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Ring Light Brightness
|
// Ring Light Brightness
|
||||||
ringLightBrightnessSlider
|
ringLightBrightnessSlider
|
||||||
@ -61,9 +66,11 @@ struct SettingsView: View {
|
|||||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
// Camera Position
|
// Camera Position
|
||||||
cameraPositionPicker
|
cameraPositionPicker
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Flash Mode
|
// Flash Mode
|
||||||
flashModePicker
|
flashModePicker
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Flash Sync (premium)
|
// Flash Sync (premium)
|
||||||
premiumToggle(
|
premiumToggle(
|
||||||
@ -72,9 +79,11 @@ struct SettingsView: View {
|
|||||||
isOn: $viewModel.isFlashSyncedWithRingLight,
|
isOn: $viewModel.isFlashSyncedWithRingLight,
|
||||||
accessibilityHint: String(localized: "Syncs flash color with ring light color")
|
accessibilityHint: String(localized: "Syncs flash color with ring light color")
|
||||||
)
|
)
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// HDR Mode
|
// HDR Mode
|
||||||
hdrModePicker
|
hdrModePicker
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Center Stage (premium feature)
|
// Center Stage (premium feature)
|
||||||
premiumToggle(
|
premiumToggle(
|
||||||
@ -83,9 +92,12 @@ struct SettingsView: View {
|
|||||||
isOn: $viewModel.isCenterStageEnabled,
|
isOn: $viewModel.isCenterStageEnabled,
|
||||||
accessibilityHint: String(localized: "Automatically adjusts camera to keep subject centered")
|
accessibilityHint: String(localized: "Automatically adjusts camera to keep subject centered")
|
||||||
)
|
)
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Photo Quality
|
// Photo Quality
|
||||||
photoQualityPicker
|
SettingsCardRow {
|
||||||
|
photoQualityPicker
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Display Section
|
// MARK: - Display Section
|
||||||
@ -101,6 +113,7 @@ struct SettingsView: View {
|
|||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot"))
|
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot"))
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// True Mirror (premium)
|
// True Mirror (premium)
|
||||||
premiumToggle(
|
premiumToggle(
|
||||||
@ -109,6 +122,7 @@ struct SettingsView: View {
|
|||||||
isOn: $viewModel.isMirrorFlipped,
|
isOn: $viewModel.isMirrorFlipped,
|
||||||
accessibilityHint: String(localized: "Flips the camera preview horizontally")
|
accessibilityHint: String(localized: "Flips the camera preview horizontally")
|
||||||
)
|
)
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Skin Smoothing (premium)
|
// Skin Smoothing (premium)
|
||||||
premiumToggle(
|
premiumToggle(
|
||||||
@ -132,9 +146,12 @@ struct SettingsView: View {
|
|||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
.accessibilityHint(String(localized: "When enabled, photos and videos are saved immediately after capture"))
|
.accessibilityHint(String(localized: "When enabled, photos and videos are saved immediately after capture"))
|
||||||
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Timer Selection
|
// Timer Selection
|
||||||
timerPicker
|
SettingsCardRow {
|
||||||
|
timerPicker
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +275,6 @@ struct SettingsView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, Design.Spacing.xSmall)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Flash Mode Picker
|
// MARK: - Flash Mode Picker
|
||||||
@ -585,40 +601,44 @@ struct SettingsView: View {
|
|||||||
// MARK: - Branding Debug Section
|
// MARK: - Branding Debug Section
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
@ViewBuilder
|
||||||
private var brandingDebugSection: some View {
|
private var brandingDebugSection: some View {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
// Debug Premium Toggle
|
||||||
// Debug Premium Toggle
|
SettingsToggle(
|
||||||
SettingsToggle(
|
title: "Enable Debug Premium",
|
||||||
title: "Enable Debug Premium",
|
subtitle: "Unlock all premium features for testing",
|
||||||
subtitle: "Unlock all premium features for testing",
|
isOn: $viewModel.isDebugPremiumEnabled,
|
||||||
isOn: $viewModel.isDebugPremiumEnabled,
|
accentColor: AppAccent.primary
|
||||||
accentColor: AppAccent.primary
|
)
|
||||||
)
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
|
|
||||||
// Reset Onboarding Button
|
// Reset Onboarding Button
|
||||||
|
SettingsCardRow {
|
||||||
ResetOnboardingButton()
|
ResetOnboardingButton()
|
||||||
|
}
|
||||||
// Icon Generator
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
SettingsNavigationRow(
|
|
||||||
title: "Icon Generator",
|
// Icon Generator
|
||||||
subtitle: "Generate and save app icon to Files",
|
SettingsNavigationRow(
|
||||||
backgroundColor: AppSurface.primary
|
title: "Icon Generator",
|
||||||
) {
|
subtitle: "Generate and save app icon to Files",
|
||||||
IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
|
backgroundColor: .clear
|
||||||
}
|
) {
|
||||||
|
IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
|
||||||
// Branding Preview
|
}
|
||||||
SettingsNavigationRow(
|
SettingsDivider(color: AppBorder.subtle)
|
||||||
title: "Branding Preview",
|
|
||||||
subtitle: "Preview app icon and launch screen",
|
// Branding Preview
|
||||||
backgroundColor: AppSurface.primary
|
SettingsNavigationRow(
|
||||||
) {
|
title: "Branding Preview",
|
||||||
BrandingPreviewView(
|
subtitle: "Preview app icon and launch screen",
|
||||||
iconConfig: .selfieCam,
|
backgroundColor: .clear
|
||||||
launchConfig: .selfieCam,
|
) {
|
||||||
appName: "SelfieCam"
|
BrandingPreviewView(
|
||||||
)
|
iconConfig: .selfieCam,
|
||||||
}
|
launchConfig: .selfieCam,
|
||||||
|
appName: "SelfieCam"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user