Refactor SettingsView to use Bedrock components: SettingsCard, SettingsNavigationRow, SettingsSlider, and titleAccessory
This commit is contained in:
parent
003d933521
commit
1b92467ec3
@ -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<Bool>,
|
||||
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<Content: View>: 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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user