diff --git a/SelfieCam/Features/Settings/SettingsView.swift b/SelfieCam/Features/Settings/SettingsView.swift index 97167ca..6862cac 100644 --- a/SelfieCam/Features/Settings/SettingsView.swift +++ b/SelfieCam/Features/Settings/SettingsView.swift @@ -30,7 +30,7 @@ struct SettingsView: View { SettingsSectionHeader(title: "Ring Light", systemImage: "light.max", accentColor: AppAccent.primary) - SettingsCard { + SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { // Ring Light Enabled SettingsToggle( title: String(localized: "Enable Ring Light"), @@ -54,7 +54,7 @@ struct SettingsView: View { SettingsSectionHeader(title: "Camera Controls", systemImage: "camera", accentColor: AppAccent.primary) - SettingsCard { + SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { // Camera Position cameraPositionPicker @@ -83,7 +83,7 @@ struct SettingsView: View { SettingsSectionHeader(title: "Display", systemImage: "eye", accentColor: AppAccent.primary) - SettingsCard { + SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { // True Mirror (premium) premiumToggle( title: String(localized: "True Mirror"), @@ -113,7 +113,7 @@ struct SettingsView: View { SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle", accentColor: AppAccent.primary) - SettingsCard { + SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { // Timer Selection timerPicker @@ -136,7 +136,7 @@ struct SettingsView: View { SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud", accentColor: AppAccent.primary) - SettingsCard { + SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { iCloudSyncSection } @@ -151,7 +151,7 @@ struct SettingsView: View { #if DEBUG SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error) - SettingsCard { + SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { brandingDebugSection } #endif @@ -175,57 +175,35 @@ struct SettingsView: View { } // MARK: - Ring Size Slider - + private var ringSizeSlider: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - HStack { - Text(String(localized: "Ring Size")) - .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) - .foregroundStyle(.white) - - Spacer() - - Text("\(Int(viewModel.ringSize))pt") - .font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - HStack(spacing: Design.Spacing.medium) { - // Small ring icon - Image(systemName: "circle") - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Slider( - value: $viewModel.ringSize, - in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize, - step: 5 - ) - .tint(AppAccent.primary) - - // Large ring icon - Image(systemName: "circle") - .font(.system(size: Design.BaseFontSize.large)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - Text(String(localized: "Adjusts the size of the light ring around the camera preview")) - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .padding(.vertical, Design.Spacing.xSmall) + SettingsSlider( + title: String(localized: "Ring Size"), + subtitle: String(localized: "Adjusts the size of the light ring around the camera preview"), + value: $viewModel.ringSize, + in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize, + step: 5, + format: SliderFormat.integer(unit: "pt"), + accentColor: AppAccent.primary, + leadingIcon: Image(systemName: "circle"), + trailingIcon: Image(systemName: "circle") + ) .accessibilityLabel(String(localized: "Ring size")) .accessibilityValue("\(Int(viewModel.ringSize)) points") } // MARK: - Color Preset Section - + private var colorPresetSection: some View { VStack(alignment: .leading, spacing: Design.Spacing.small) { Text(String(localized: "Light Color")) .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) .foregroundStyle(.white) - + + Text(String(localized: "Choose the color of the ring light around the camera preview")) + .font(.system(size: Design.BaseFontSize.caption)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + LazyVGrid( columns: [GridItem(.adaptive(minimum: 80), spacing: Design.Spacing.small)], spacing: Design.Spacing.small @@ -248,7 +226,7 @@ struct SettingsView: View { } } } - + // Custom color picker (premium) - one-step: opens picker, selects on change CustomColorPickerButton( customColor: Binding( @@ -410,41 +388,17 @@ struct SettingsView: View { // MARK: - Ring Light Brightness Slider private var ringLightBrightnessSlider: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - HStack { - Text(String(localized: "Ring Light Brightness")) - .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) - .foregroundStyle(.white) - - Spacer() - - Text("\(Int(viewModel.ringLightOpacity * 100))%") - .font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - HStack(spacing: Design.Spacing.medium) { - Image(systemName: "sun.min") - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Slider( - value: $viewModel.ringLightOpacity, - in: 0.1...1.0, - step: 0.05 - ) - .tint(AppAccent.primary) - - Image(systemName: "sun.max.fill") - .font(.system(size: Design.BaseFontSize.large)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - Text(String(localized: "Adjusts the brightness of the ring light")) - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .padding(.vertical, Design.Spacing.xSmall) + SettingsSlider( + title: String(localized: "Ring Light Brightness"), + subtitle: String(localized: "Adjusts the brightness of the ring light"), + value: $viewModel.ringLightOpacity, + in: 0.1...1.0, + step: 0.05, + format: SliderFormat.percentage, + accentColor: AppAccent.primary, + leadingIcon: Image(systemName: "sun.min"), + trailingIcon: Image(systemName: "sun.max.fill") + ) .accessibilityLabel(String(localized: "Ring light brightness")) .accessibilityValue("\(Int(viewModel.ringLightOpacity * 100)) percent") } @@ -668,31 +622,12 @@ struct SettingsView: View { // MARK: - Acknowledgments Section private var acknowledgmentsSection: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - NavigationLink { - LicensesView() - } label: { - HStack { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text(String(localized: "Open Source Licenses")) - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.white) - - Text(String(localized: "Third-party libraries used in this app")) - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - Spacer() - - Image(systemName: "chevron.right") - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .padding(Design.Spacing.medium) - .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) - } - .buttonStyle(.plain) + SettingsNavigationRow( + title: String(localized: "Open Source Licenses"), + subtitle: String(localized: "Third-party libraries used in this app"), + backgroundColor: AppSurface.primary + ) { + LicensesView() } } @@ -712,25 +647,16 @@ struct SettingsView: View { isOn: Binding, accessibilityHint: String ) -> some View { - Toggle(isOn: isOn) { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - HStack(spacing: Design.Spacing.xSmall) { - Text(title) - .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) - .foregroundStyle(.white) - - Image(systemName: "crown.fill") - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(AppStatus.warning) - } - - Text(subtitle) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) + SettingsToggle( + title: title, + subtitle: subtitle, + isOn: isOn, + accentColor: AppAccent.primary, + titleAccessory: { + Image(systemName: "crown.fill") + .foregroundStyle(AppStatus.warning) } - } - .tint(AppAccent.primary) - .padding(.vertical, Design.Spacing.xSmall) + ) .disabled(!isPremiumUnlocked) .accessibilityHint(accessibilityHint) .onTapGesture { @@ -783,61 +709,28 @@ struct SettingsView: View { isOn: $viewModel.isDebugPremiumEnabled, accentColor: AppStatus.warning ) + // Icon Generator - NavigationLink { + SettingsNavigationRow( + title: "Icon Generator", + subtitle: "Generate and save app icon to Files", + backgroundColor: AppSurface.primary + ) { IconGeneratorView(config: .selfieCam, appName: "SelfieCam") - } label: { - HStack { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text("Icon Generator") - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.white) - - Text("Generate and save app icon to Files") - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - Spacer() - - Image(systemName: "chevron.right") - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .padding(Design.Spacing.medium) - .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) } - .buttonStyle(.plain) // Branding Preview - NavigationLink { + SettingsNavigationRow( + title: "Branding Preview", + subtitle: "Preview app icon and launch screen", + backgroundColor: AppSurface.primary + ) { BrandingPreviewView( iconConfig: .selfieCam, launchConfig: .selfieCam, appName: "SelfieCam" ) - } label: { - HStack { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text("Branding Preview") - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.white) - - Text("Preview app icon and launch screen") - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - Spacer() - - Image(systemName: "chevron.right") - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - .padding(Design.Spacing.medium) - .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) } - .buttonStyle(.plain) } } #endif @@ -845,28 +738,6 @@ struct SettingsView: View { -// MARK: - Settings Card Container - -/// A card container that provides visual grouping for settings sections. -/// Uses the app's branded surface colors for separation from the background. -private struct SettingsCard: View { - @ViewBuilder let content: Content - - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - content - } - .padding(Design.Spacing.medium) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.large) - .fill(AppSurface.card) - ) - .overlay( - RoundedRectangle(cornerRadius: Design.CornerRadius.large) - .strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin) - ) - } -} #Preview { SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false)) diff --git a/SelfieCam/Features/Settings/SettingsViewModel.swift b/SelfieCam/Features/Settings/SettingsViewModel.swift index deb476f..6cfb2e2 100644 --- a/SelfieCam/Features/Settings/SettingsViewModel.swift +++ b/SelfieCam/Features/Settings/SettingsViewModel.swift @@ -43,7 +43,7 @@ final class SettingsViewModel: RingLightConfigurable { // MARK: - Ring Size Limits /// Minimum ring border size in points - static let minRingSize: CGFloat = 10 + static let minRingSize: CGFloat = 40 /// Maximum ring border size in points static let maxRingSize: CGFloat = 120 diff --git a/SelfieCam/Resources/Localizable.xcstrings b/SelfieCam/Resources/Localizable.xcstrings index 6de8ed9..7145e1b 100644 --- a/SelfieCam/Resources/Localizable.xcstrings +++ b/SelfieCam/Resources/Localizable.xcstrings @@ -123,6 +123,7 @@ }, "%lldpt" : { "comment" : "A label displaying the current ring size, formatted as a number followed by the unit \"pt\".", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "es-MX" : { @@ -770,6 +771,10 @@ } } }, + "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 + }, "Close" : { "comment" : "A button label that closes the view.", "isCommentAutoGenerated" : true