From cf95c4e816ac0b2a576b5b7dc64deb29036b4e2e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sun, 4 Jan 2026 11:38:40 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../Camera/Views/ColorPickerOverlay.swift | 71 ++++++ .../Camera/Views/CustomCameraScreen.swift | 188 -------------- .../Camera/Views/OpacitySliderOverlay.swift | 72 ++++++ .../Camera/Views/PhotoReviewView.swift | 45 ---- .../Features/Camera/Views/ShareButton.swift | 36 +++ .../Features/Camera/Views/ShareSheet.swift | 26 ++ .../Camera/Views/SizeSliderOverlay.swift | 72 ++++++ .../Features/Settings/ColorPresetButton.swift | 78 ++++++ .../Settings/CustomColorPickerButton.swift | 111 +++++++++ .../Features/Settings/LicensesView.swift | 70 ++++++ .../Features/Settings/SettingsView.swift | 230 ------------------ 11 files changed, 536 insertions(+), 463 deletions(-) create mode 100644 SelfieCam/Features/Camera/Views/ColorPickerOverlay.swift create mode 100644 SelfieCam/Features/Camera/Views/OpacitySliderOverlay.swift create mode 100644 SelfieCam/Features/Camera/Views/ShareButton.swift create mode 100644 SelfieCam/Features/Camera/Views/ShareSheet.swift create mode 100644 SelfieCam/Features/Camera/Views/SizeSliderOverlay.swift create mode 100644 SelfieCam/Features/Settings/ColorPresetButton.swift create mode 100644 SelfieCam/Features/Settings/CustomColorPickerButton.swift create mode 100644 SelfieCam/Features/Settings/LicensesView.swift diff --git a/SelfieCam/Features/Camera/Views/ColorPickerOverlay.swift b/SelfieCam/Features/Camera/Views/ColorPickerOverlay.swift new file mode 100644 index 0000000..967083c --- /dev/null +++ b/SelfieCam/Features/Camera/Views/ColorPickerOverlay.swift @@ -0,0 +1,71 @@ +// +// 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) + } + } +} diff --git a/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift b/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift index d70de4f..bd0498d 100644 --- a/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift +++ b/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift @@ -493,191 +493,3 @@ struct CustomCameraScreen: MCameraScreen { isControlsExpanded = false // Collapse controls panel } } - -// 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) - } - } -} - -// 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) - } - } -} - -// MARK: - Size Slider Overlay - -struct SizeSliderOverlay: View { - @Binding var selectedSize: CGFloat - @Binding var isPresented: Bool - - private let minSize: CGFloat = 50 - private let maxSize: CGFloat = 100 - - 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) - } - } -} diff --git a/SelfieCam/Features/Camera/Views/OpacitySliderOverlay.swift b/SelfieCam/Features/Camera/Views/OpacitySliderOverlay.swift new file mode 100644 index 0000000..290d7b0 --- /dev/null +++ b/SelfieCam/Features/Camera/Views/OpacitySliderOverlay.swift @@ -0,0 +1,72 @@ +// +// 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) + } + } +} diff --git a/SelfieCam/Features/Camera/Views/PhotoReviewView.swift b/SelfieCam/Features/Camera/Views/PhotoReviewView.swift index 0011f68..f96778e 100644 --- a/SelfieCam/Features/Camera/Views/PhotoReviewView.swift +++ b/SelfieCam/Features/Camera/Views/PhotoReviewView.swift @@ -116,48 +116,3 @@ struct PhotoReviewView: View { .accessibilityHint("Use the buttons at the bottom to save or share your photo") } } - -// MARK: - Share Button - -struct ShareButton: View { - let photo: UIImage - @State private var isShareSheetPresented = false - - var body: some View { - Button(action: { - isShareSheetPresented = true - }) { - ZStack { - Circle() - .fill(Color.black.opacity(0.6)) - .frame(width: 80, height: 80) - - Image(systemName: "square.and.arrow.up") - .font(.system(size: 24, weight: .medium)) - .foregroundColor(.white) - } - .shadow(radius: 5) - } - .sheet(isPresented: $isShareSheetPresented) { - ShareSheet(activityItems: [photo]) - } - } -} - -// MARK: - Share Sheet - -struct ShareSheet: UIViewControllerRepresentable { - let activityItems: [Any] - - func makeUIViewController(context: Context) -> UIActivityViewController { - let controller = UIActivityViewController( - activityItems: activityItems, - applicationActivities: nil - ) - return controller - } - - func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { - // No updates needed - } -} diff --git a/SelfieCam/Features/Camera/Views/ShareButton.swift b/SelfieCam/Features/Camera/Views/ShareButton.swift new file mode 100644 index 0000000..2787015 --- /dev/null +++ b/SelfieCam/Features/Camera/Views/ShareButton.swift @@ -0,0 +1,36 @@ +// +// ShareButton.swift +// SelfieCam +// +// Created by Matt Bruce on 1/4/26. +// + +import SwiftUI +import Bedrock + +// MARK: - Share Button + +struct ShareButton: View { + let photo: UIImage + @State private var isShareSheetPresented = false + + var body: some View { + Button(action: { + isShareSheetPresented = true + }) { + ZStack { + Circle() + .fill(Color.black.opacity(0.6)) + .frame(width: 80, height: 80) + + Image(systemName: "square.and.arrow.up") + .font(.system(size: 24, weight: .medium)) + .foregroundColor(.white) + } + .shadow(radius: 5) + } + .sheet(isPresented: $isShareSheetPresented) { + ShareSheet(activityItems: [photo]) + } + } +} diff --git a/SelfieCam/Features/Camera/Views/ShareSheet.swift b/SelfieCam/Features/Camera/Views/ShareSheet.swift new file mode 100644 index 0000000..36de269 --- /dev/null +++ b/SelfieCam/Features/Camera/Views/ShareSheet.swift @@ -0,0 +1,26 @@ +// +// ShareSheet.swift +// SelfieCam +// +// Created by Matt Bruce on 1/4/26. +// + +import SwiftUI + +// MARK: - Share Sheet + +struct ShareSheet: UIViewControllerRepresentable { + let activityItems: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController( + activityItems: activityItems, + applicationActivities: nil + ) + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { + // No updates needed + } +} diff --git a/SelfieCam/Features/Camera/Views/SizeSliderOverlay.swift b/SelfieCam/Features/Camera/Views/SizeSliderOverlay.swift new file mode 100644 index 0000000..03e3b3e --- /dev/null +++ b/SelfieCam/Features/Camera/Views/SizeSliderOverlay.swift @@ -0,0 +1,72 @@ +// +// 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 = 50 + private let maxSize: CGFloat = 100 + + 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) + } + } +} diff --git a/SelfieCam/Features/Settings/ColorPresetButton.swift b/SelfieCam/Features/Settings/ColorPresetButton.swift new file mode 100644 index 0000000..6b614bd --- /dev/null +++ b/SelfieCam/Features/Settings/ColorPresetButton.swift @@ -0,0 +1,78 @@ +// +// ColorPresetButton.swift +// SelfieCam +// +// Created by Matt Bruce on 1/4/26. +// + +import SwiftUI +import Bedrock + +// MARK: - Color Preset Button + +struct ColorPresetButton: View { + let preset: RingLightColor + let isSelected: Bool + let isPremiumUnlocked: Bool + let action: () -> Void + + /// Whether this premium color is locked (not available) + private var isLocked: Bool { + preset.isPremium && !isPremiumUnlocked + } + + var body: some View { + Button(action: action) { + VStack(spacing: Design.Spacing.xxSmall) { + ZStack { + Circle() + .fill(preset.color) + .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) + .overlay( + Circle() + .strokeBorder( + isSelected ? Color.Accent.primary : Color.Border.subtle, + lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin + ) + ) + .shadow( + color: preset.color.opacity(Design.Opacity.light), + radius: isSelected ? Design.Shadow.radiusSmall : 0 + ) + + // Lock overlay for locked premium colors + if isLocked { + Circle() + .fill(.black.opacity(Design.Opacity.medium)) + .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) + + Image(systemName: "lock.fill") + .font(.system(size: Design.BaseFontSize.small)) + .foregroundStyle(.white) + } + } + + Text(preset.name) + .font(.system(size: Design.BaseFontSize.xSmall)) + .foregroundStyle(.white.opacity(isSelected ? 1.0 : (isLocked ? Design.Opacity.medium : Design.Opacity.accent))) + .lineLimit(1) + .minimumScaleFactor(Design.MinScaleFactor.tight) + + if preset.isPremium { + Image(systemName: isPremiumUnlocked ? "crown.fill" : "crown") + .font(.system(size: Design.BaseFontSize.xxSmall)) + .foregroundStyle(isPremiumUnlocked ? Color.Status.warning : Color.Status.warning.opacity(Design.Opacity.medium)) + } + } + .padding(Design.Spacing.xSmall) + .background( + RoundedRectangle(cornerRadius: Design.CornerRadius.small) + .fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear) + ) + } + .buttonStyle(.plain) + .accessibilityLabel(preset.name) + .accessibilityAddTraits(isSelected ? .isSelected : []) + .accessibilityHint(isLocked ? String(localized: "Locked. Tap to unlock with Pro.") : (preset.isPremium ? String(localized: "Premium color") : "")) + } +} diff --git a/SelfieCam/Features/Settings/CustomColorPickerButton.swift b/SelfieCam/Features/Settings/CustomColorPickerButton.swift new file mode 100644 index 0000000..df2ad71 --- /dev/null +++ b/SelfieCam/Features/Settings/CustomColorPickerButton.swift @@ -0,0 +1,111 @@ +// +// CustomColorPickerButton.swift +// SelfieCam +// +// Created by Matt Bruce on 1/4/26. +// + +import SwiftUI +import Bedrock + +// MARK: - Custom Color Picker Button + +/// Custom color picker with premium gating +struct CustomColorPickerButton: View { + @Binding var customColor: Color + let isSelected: Bool + let isPremiumUnlocked: Bool + let onPremiumRequired: () -> Void + + /// Whether the custom color is locked + private var isLocked: Bool { !isPremiumUnlocked } + + var body: some View { + if isPremiumUnlocked { + // Premium users get the full color picker + VStack(spacing: Design.Spacing.xxSmall) { + ColorPicker( + selection: $customColor, + supportsOpacity: false + ) { + EmptyView() + } + .labelsHidden() + .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) + .clipShape(.circle) + .overlay( + Circle() + .strokeBorder( + isSelected ? Color.Accent.primary : Color.Border.subtle, + lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin + ) + ) + .shadow( + color: customColor.opacity(Design.Opacity.light), + radius: isSelected ? Design.Shadow.radiusSmall : 0 + ) + + Text(String(localized: "Custom")) + .font(.system(size: Design.BaseFontSize.xSmall)) + .foregroundStyle(.white.opacity(isSelected ? 1.0 : Design.Opacity.accent)) + .lineLimit(1) + .minimumScaleFactor(Design.MinScaleFactor.tight) + + Image(systemName: "crown.fill") + .font(.system(size: Design.BaseFontSize.xxSmall)) + .foregroundStyle(Color.Status.warning) + } + .padding(Design.Spacing.xSmall) + .background( + RoundedRectangle(cornerRadius: Design.CornerRadius.small) + .fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear) + ) + .accessibilityLabel(String(localized: "Custom color")) + .accessibilityAddTraits(isSelected ? .isSelected : []) + } else { + // Non-premium users see a locked button that shows paywall + Button(action: onPremiumRequired) { + VStack(spacing: Design.Spacing.xxSmall) { + ZStack { + // Rainbow gradient to show what's possible + Circle() + .fill( + AngularGradient( + colors: [.red, .orange, .yellow, .green, .blue, .purple, .red], + center: .center + ) + ) + .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) + .overlay( + Circle() + .strokeBorder(Color.Border.subtle, lineWidth: Design.LineWidth.thin) + ) + + // Lock overlay + Circle() + .fill(.black.opacity(Design.Opacity.medium)) + .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) + + Image(systemName: "lock.fill") + .font(.system(size: Design.BaseFontSize.small)) + .foregroundStyle(.white) + } + + Text(String(localized: "Custom")) + .font(.system(size: Design.BaseFontSize.xSmall)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + .lineLimit(1) + .minimumScaleFactor(Design.MinScaleFactor.tight) + + Image(systemName: "crown") + .font(.system(size: Design.BaseFontSize.xxSmall)) + .foregroundStyle(Color.Status.warning.opacity(Design.Opacity.medium)) + } + .padding(Design.Spacing.xSmall) + } + .buttonStyle(.plain) + .accessibilityLabel(String(localized: "Custom color")) + .accessibilityHint(String(localized: "Locked. Tap to unlock with Pro.")) + } + } +} diff --git a/SelfieCam/Features/Settings/LicensesView.swift b/SelfieCam/Features/Settings/LicensesView.swift new file mode 100644 index 0000000..368dcca --- /dev/null +++ b/SelfieCam/Features/Settings/LicensesView.swift @@ -0,0 +1,70 @@ +// +// LicensesView.swift +// SelfieCam +// +// Created by Matt Bruce on 1/4/26. +// + +import SwiftUI +import Bedrock + +// MARK: - Licenses View + +struct LicensesView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: Design.Spacing.large) { + // MijickCamera + licenseCard( + name: "MijickCamera", + url: "https://github.com/Mijick/Camera", + license: "Apache 2.0 License", + description: "Camera framework for SwiftUI. Created by Tomasz Kurylik at Mijick." + ) + + // RevenueCat + licenseCard( + name: "RevenueCat", + url: "https://github.com/RevenueCat/purchases-ios", + license: "MIT License", + description: "In-app subscriptions made easy." + ) + } + .padding(Design.Spacing.large) + } + .background(Color.Surface.overlay) + .navigationTitle(String(localized: "Open Source Licenses")) + .navigationBarTitleDisplayMode(.inline) + } + + private func licenseCard(name: String, url: String, license: String, description: String) -> some View { + VStack(alignment: .leading, spacing: Design.Spacing.small) { + Text(name) + .font(.system(size: Design.BaseFontSize.medium, weight: .bold)) + .foregroundStyle(.white) + + Text(description) + .font(.system(size: Design.BaseFontSize.caption)) + .foregroundStyle(.white.opacity(Design.Opacity.strong)) + + HStack { + Label(license, systemImage: "doc.text") + .font(.system(size: Design.BaseFontSize.xSmall)) + .foregroundStyle(Color.Accent.primary) + + Spacer() + + if let linkURL = URL(string: url) { + Link(destination: linkURL) { + Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square") + .font(.system(size: Design.BaseFontSize.xSmall)) + .foregroundStyle(Color.Accent.primary) + } + } + } + .padding(.top, Design.Spacing.xSmall) + } + .padding(Design.Spacing.medium) + .background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) + } +} diff --git a/SelfieCam/Features/Settings/SettingsView.swift b/SelfieCam/Features/Settings/SettingsView.swift index b462f8f..c287eba 100644 --- a/SelfieCam/Features/Settings/SettingsView.swift +++ b/SelfieCam/Features/Settings/SettingsView.swift @@ -483,237 +483,7 @@ struct SettingsView: View { } } -// MARK: - Licenses View -struct LicensesView: View { - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: Design.Spacing.large) { - // MijickCamera - licenseCard( - name: "MijickCamera", - url: "https://github.com/Mijick/Camera", - license: "Apache 2.0 License", - description: "Camera framework for SwiftUI. Created by Tomasz Kurylik at Mijick." - ) - - // RevenueCat - licenseCard( - name: "RevenueCat", - url: "https://github.com/RevenueCat/purchases-ios", - license: "MIT License", - description: "In-app subscriptions made easy." - ) - } - .padding(Design.Spacing.large) - } - .background(Color.Surface.overlay) - .navigationTitle(String(localized: "Open Source Licenses")) - .navigationBarTitleDisplayMode(.inline) - } - - private func licenseCard(name: String, url: String, license: String, description: String) -> some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - Text(name) - .font(.system(size: Design.BaseFontSize.medium, weight: .bold)) - .foregroundStyle(.white) - - Text(description) - .font(.system(size: Design.BaseFontSize.caption)) - .foregroundStyle(.white.opacity(Design.Opacity.strong)) - - HStack { - Label(license, systemImage: "doc.text") - .font(.system(size: Design.BaseFontSize.xSmall)) - .foregroundStyle(Color.Accent.primary) - - Spacer() - - if let linkURL = URL(string: url) { - Link(destination: linkURL) { - Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square") - .font(.system(size: Design.BaseFontSize.xSmall)) - .foregroundStyle(Color.Accent.primary) - } - } - } - .padding(.top, Design.Spacing.xSmall) - } - .padding(Design.Spacing.medium) - .background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) - } -} - -// MARK: - Color Preset Button - -private struct ColorPresetButton: View { - let preset: RingLightColor - let isSelected: Bool - let isPremiumUnlocked: Bool - let action: () -> Void - - /// Whether this premium color is locked (not available) - private var isLocked: Bool { - preset.isPremium && !isPremiumUnlocked - } - - var body: some View { - Button(action: action) { - VStack(spacing: Design.Spacing.xxSmall) { - ZStack { - Circle() - .fill(preset.color) - .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) - .overlay( - Circle() - .strokeBorder( - isSelected ? Color.Accent.primary : Color.Border.subtle, - lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin - ) - ) - .shadow( - color: preset.color.opacity(Design.Opacity.light), - radius: isSelected ? Design.Shadow.radiusSmall : 0 - ) - - // Lock overlay for locked premium colors - if isLocked { - Circle() - .fill(.black.opacity(Design.Opacity.medium)) - .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) - - Image(systemName: "lock.fill") - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white) - } - } - - Text(preset.name) - .font(.system(size: Design.BaseFontSize.xSmall)) - .foregroundStyle(.white.opacity(isSelected ? 1.0 : (isLocked ? Design.Opacity.medium : Design.Opacity.accent))) - .lineLimit(1) - .minimumScaleFactor(Design.MinScaleFactor.tight) - - if preset.isPremium { - Image(systemName: isPremiumUnlocked ? "crown.fill" : "crown") - .font(.system(size: Design.BaseFontSize.xxSmall)) - .foregroundStyle(isPremiumUnlocked ? Color.Status.warning : Color.Status.warning.opacity(Design.Opacity.medium)) - } - } - .padding(Design.Spacing.xSmall) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.small) - .fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear) - ) - } - .buttonStyle(.plain) - .accessibilityLabel(preset.name) - .accessibilityAddTraits(isSelected ? .isSelected : []) - .accessibilityHint(isLocked ? String(localized: "Locked. Tap to unlock with Pro.") : (preset.isPremium ? String(localized: "Premium color") : "")) - } -} - -// MARK: - Custom Color Picker Button - -/// Custom color picker with premium gating -private struct CustomColorPickerButton: View { - @Binding var customColor: Color - let isSelected: Bool - let isPremiumUnlocked: Bool - let onPremiumRequired: () -> Void - - /// Whether the custom color is locked - private var isLocked: Bool { !isPremiumUnlocked } - - var body: some View { - if isPremiumUnlocked { - // Premium users get the full color picker - VStack(spacing: Design.Spacing.xxSmall) { - ColorPicker( - selection: $customColor, - supportsOpacity: false - ) { - EmptyView() - } - .labelsHidden() - .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) - .clipShape(.circle) - .overlay( - Circle() - .strokeBorder( - isSelected ? Color.Accent.primary : Color.Border.subtle, - lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin - ) - ) - .shadow( - color: customColor.opacity(Design.Opacity.light), - radius: isSelected ? Design.Shadow.radiusSmall : 0 - ) - - Text(String(localized: "Custom")) - .font(.system(size: Design.BaseFontSize.xSmall)) - .foregroundStyle(.white.opacity(isSelected ? 1.0 : Design.Opacity.accent)) - .lineLimit(1) - .minimumScaleFactor(Design.MinScaleFactor.tight) - - Image(systemName: "crown.fill") - .font(.system(size: Design.BaseFontSize.xxSmall)) - .foregroundStyle(Color.Status.warning) - } - .padding(Design.Spacing.xSmall) - .background( - RoundedRectangle(cornerRadius: Design.CornerRadius.small) - .fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear) - ) - .accessibilityLabel(String(localized: "Custom color")) - .accessibilityAddTraits(isSelected ? .isSelected : []) - } else { - // Non-premium users see a locked button that shows paywall - Button(action: onPremiumRequired) { - VStack(spacing: Design.Spacing.xxSmall) { - ZStack { - // Rainbow gradient to show what's possible - Circle() - .fill( - AngularGradient( - colors: [.red, .orange, .yellow, .green, .blue, .purple, .red], - center: .center - ) - ) - .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) - .overlay( - Circle() - .strokeBorder(Color.Border.subtle, lineWidth: Design.LineWidth.thin) - ) - - // Lock overlay - Circle() - .fill(.black.opacity(Design.Opacity.medium)) - .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) - - Image(systemName: "lock.fill") - .font(.system(size: Design.BaseFontSize.small)) - .foregroundStyle(.white) - } - - Text(String(localized: "Custom")) - .font(.system(size: Design.BaseFontSize.xSmall)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - .lineLimit(1) - .minimumScaleFactor(Design.MinScaleFactor.tight) - - Image(systemName: "crown") - .font(.system(size: Design.BaseFontSize.xxSmall)) - .foregroundStyle(Color.Status.warning.opacity(Design.Opacity.medium)) - } - .padding(Design.Spacing.xSmall) - } - .buttonStyle(.plain) - .accessibilityLabel(String(localized: "Custom color")) - .accessibilityHint(String(localized: "Locked. Tap to unlock with Pro.")) - } - } -} #Preview { SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))