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