Compare commits

..

No commits in common. "e824119dcb73d9ce39bcc53cdb3cf4828d1d4b02" and "e628897bfff6a091b2fe9d4da7f922d2894b6344" have entirely different histories.

5 changed files with 519 additions and 187 deletions

View File

@ -9,8 +9,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C852F08306200DC03E1 /* RevenueCat */; }; EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C852F08306200DC03E1 /* RevenueCat */; };
EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C872F08306200DC03E1 /* RevenueCatUI */; }; EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C872F08306200DC03E1 /* RevenueCatUI */; };
EA836AB22F0ACBCF00077F87 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AB12F0ACBCF00077F87 /* Bedrock */; }; EA766F022F08500000DC03E1 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA766F012F08500000DC03E1 /* Bedrock */; };
EA836AB52F0ACBE600077F87 /* MijickCamera in Frameworks */ = {isa = PBXBuildFile; productRef = EA836AB42F0ACBE600077F87 /* MijickCamera */; }; EA766F102F08600000DC03E1 /* MijickCamera in Frameworks */ = {isa = PBXBuildFile; productRef = EA766F0F2F08600000DC03E1 /* MijickCamera */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -61,10 +61,10 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EA836AB52F0ACBE600077F87 /* MijickCamera in Frameworks */,
EA836AB22F0ACBCF00077F87 /* Bedrock in Frameworks */,
EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */, EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */,
EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */, EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */,
EA766F022F08500000DC03E1 /* Bedrock in Frameworks */,
EA766F102F08600000DC03E1 /* MijickCamera in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -137,8 +137,8 @@
packageProductDependencies = ( packageProductDependencies = (
EA766C852F08306200DC03E1 /* RevenueCat */, EA766C852F08306200DC03E1 /* RevenueCat */,
EA766C872F08306200DC03E1 /* RevenueCatUI */, EA766C872F08306200DC03E1 /* RevenueCatUI */,
EA836AB12F0ACBCF00077F87 /* Bedrock */, EA766F012F08500000DC03E1 /* Bedrock */,
EA836AB42F0ACBE600077F87 /* MijickCamera */, EA766F0F2F08600000DC03E1 /* MijickCamera */,
); );
productName = SelfieRingLight; productName = SelfieRingLight;
productReference = EA766C2C2F082A8400DC03E1 /* SelfieRingLight.app */; productReference = EA766C2C2F082A8400DC03E1 /* SelfieRingLight.app */;
@ -224,8 +224,8 @@
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
EA766C822F08306200DC03E1 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */, EA766C822F08306200DC03E1 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */,
EA836AB02F0ACBCF00077F87 /* XCRemoteSwiftPackageReference "Bedrock" */, EA766F002F08500000DC03E1 /* XCRemoteSwiftPackageReference "Bedrock" */,
EA836AB32F0ACBE600077F87 /* XCRemoteSwiftPackageReference "MijickCamera" */, EA766F0E2F08600000DC03E1 /* XCRemoteSwiftPackageReference "Camera" */,
); );
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = EA766C2D2F082A8400DC03E1 /* Products */; productRefGroup = EA766C2D2F082A8400DC03E1 /* Products */;
@ -634,22 +634,22 @@
minimumVersion = 5.52.1; minimumVersion = 5.52.1;
}; };
}; };
EA836AB02F0ACBCF00077F87 /* XCRemoteSwiftPackageReference "Bedrock" */ = { EA766F002F08500000DC03E1 /* XCRemoteSwiftPackageReference "Bedrock" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "http://192.168.1.128:3000/mbrucedogs/Bedrock"; repositoryURL = "ssh://git@192.168.1.128:220/mbrucedogs/Bedrock.git";
requirement = {
branch = master;
kind = branch;
};
};
EA836AB32F0ACBE600077F87 /* XCRemoteSwiftPackageReference "MijickCamera" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "http://192.168.1.128:3000/mbrucedogs/MijickCamera";
requirement = { requirement = {
branch = develop; branch = develop;
kind = branch; kind = branch;
}; };
}; };
EA766F0E2F08600000DC03E1 /* XCRemoteSwiftPackageReference "Camera" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Mijick/Camera";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -663,14 +663,14 @@
package = EA766C822F08306200DC03E1 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */; package = EA766C822F08306200DC03E1 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */;
productName = RevenueCatUI; productName = RevenueCatUI;
}; };
EA836AB12F0ACBCF00077F87 /* Bedrock */ = { EA766F012F08500000DC03E1 /* Bedrock */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = EA836AB02F0ACBCF00077F87 /* XCRemoteSwiftPackageReference "Bedrock" */; package = EA766F002F08500000DC03E1 /* XCRemoteSwiftPackageReference "Bedrock" */;
productName = Bedrock; productName = Bedrock;
}; };
EA836AB42F0ACBE600077F87 /* MijickCamera */ = { EA766F0F2F08600000DC03E1 /* MijickCamera */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = EA836AB32F0ACBE600077F87 /* XCRemoteSwiftPackageReference "MijickCamera" */; package = EA766F0E2F08600000DC03E1 /* XCRemoteSwiftPackageReference "Camera" */;
productName = MijickCamera; productName = MijickCamera;
}; };
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */

View File

@ -1,104 +1,287 @@
import SwiftUI import SwiftUI
import MijickCamera import MijickCamera
import Bedrock import Bedrock
import Photos
import AVFoundation
/// Main camera view with ring light effect using MijickCamera
struct ContentView: View { struct ContentView: View {
@State private var settings = SettingsViewModel() @State private var settings = SettingsViewModel()
@State private var premiumManager = PremiumManager() @State private var premiumManager = PremiumManager()
@State private var showSettings = false
@State private var showPaywall = false @State private var showPaywall = false
@State private var showSettings = false
@State private var toastMessage: String?
// Post-capture state // Captured media for post-capture preview
@State private var capturedImage: UIImage? @State private var capturedImage: UIImage?
@State private var capturedVideoURL: URL? @State private var capturedVideoURL: URL?
@State private var showPostCapture = false @State private var showPostCapturePreview = false
/// Ring size clamped to reasonable max // Center Stage support
private var effectiveRingSize: CGFloat { @State private var isCenterStageAvailable = false
let maxRing = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * 0.2 @State private var isCenterStageEnabled = false
return min(settings.ringSize, maxRing)
}
var body: some View { var body: some View {
ZStack { GeometryReader { geometry in
// Ring light background let maxRingSize = calculateMaxRingSize(for: geometry)
settings.lightColor let effectiveRingSize = min(settings.ringSize, maxRingSize)
.ignoresSafeArea()
// MijickCamera with default UI // Use MCamera as the base, with ring light as border
MCamera() MCamera()
.setCameraPosition(.front) // Default to front camera for selfies
.onImageCaptured { image, _ in .onImageCaptured { image, _ in
capturedImage = image handleImageCaptured(image)
showPostCapture = true
} }
.onVideoCaptured { url, _ in .onVideoCaptured { url, _ in
capturedVideoURL = url handleVideoCaptured(url)
showPostCapture = true
} }
.startSession() .ignoresSafeArea()
.padding(.horizontal, effectiveRingSize) .overlay {
.padding(.top, effectiveRingSize) // Ring light border overlay
.padding(.bottom, effectiveRingSize) ringLightOverlay(ringSize: effectiveRingSize)
}
// Settings button overlay (top right corner of camera area) .overlay {
VStack { // Grid overlay
HStack { if settings.isGridVisible {
Spacer() GridOverlay(isVisible: true)
.padding(effectiveRingSize)
Button { .allowsHitTesting(false)
showSettings = true }
} label: { }
Image(systemName: "gearshape.fill") .overlay {
.font(.title3) // Top controls
.foregroundStyle(.white) VStack {
.padding(Design.Spacing.medium) topControlBar
.background(.ultraThinMaterial, in: Circle()) .padding(.top, effectiveRingSize + Design.Spacing.small)
.shadow(radius: Design.Shadow.radiusSmall) Spacer()
}
.padding(.horizontal, effectiveRingSize + Design.Spacing.small)
}
.overlay {
// Toast
if let message = toastMessage {
toastView(message: message)
}
}
.onChange(of: geometry.size) { _, newSize in
let newMax = min(newSize.width, newSize.height) / 4
if settings.ringSize > newMax {
settings.ringSize = newMax
} }
.accessibilityLabel("Settings")
} }
.padding(.horizontal, effectiveRingSize + Design.Spacing.medium)
.padding(.top, effectiveRingSize + Design.Spacing.medium)
Spacer()
}
} }
.ignoresSafeArea() .onAppear {
.sheet(isPresented: $showSettings) { checkCenterStageAvailability()
SettingsView(viewModel: settings, showPaywall: $showPaywall)
} }
.sheet(isPresented: $showPaywall) { .sheet(isPresented: $showPaywall) {
ProPaywallView() ProPaywallView()
} }
.fullScreenCover(isPresented: $showPostCapture) { .sheet(isPresented: $showSettings) {
PostCapturePreviewView( SettingsView(viewModel: settings, showPaywall: $showPaywall)
capturedImage: capturedImage, }
capturedVideoURL: capturedVideoURL, .fullScreenCover(isPresented: $showPostCapturePreview) {
isAutoSaveEnabled: settings.isAutoSaveEnabled, postCaptureView
onRetake: { }
capturedImage = nil }
capturedVideoURL = nil
showPostCapture = false // MARK: - Max Ring Size Calculation
},
onSave: { private func calculateMaxRingSize(for geometry: GeometryProxy) -> CGFloat {
saveCapture() min(geometry.size.width, geometry.size.height) / 4
showPostCapture = false }
// MARK: - Ring Light Overlay
/// Creates a ring light effect as a border around the camera
@ViewBuilder
private func ringLightOverlay(ringSize: CGFloat) -> some View {
// Use a rectangle with a large inner cutout to create the ring effect
GeometryReader { geo in
let innerRect = CGRect(
x: ringSize,
y: ringSize,
width: geo.size.width - (ringSize * 2),
height: geo.size.height - (ringSize * 2)
)
settings.lightColor
.mask(
Rectangle()
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.frame(width: innerRect.width, height: innerRect.height)
.blendMode(.destinationOut)
)
.compositingGroup()
)
.allowsHitTesting(false)
}
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
}
// MARK: - Top Control Bar
private var topControlBar: some View {
HStack {
// Center Stage button (only shown when available)
if isCenterStageAvailable {
Button {
toggleCenterStage()
} label: {
Image(systemName: isCenterStageEnabled ? "person.crop.rectangle.fill" : "person.crop.rectangle")
.font(.body)
.foregroundStyle(isCenterStageEnabled ? .yellow : .white)
.padding(Design.Spacing.small)
.background(.ultraThinMaterial, in: Circle())
} }
.accessibilityLabel(String(localized: "Center Stage"))
.accessibilityValue(isCenterStageEnabled ? "On" : "Off")
.accessibilityHint(String(localized: "Keeps you centered in frame"))
}
Spacer()
// Grid toggle
Button {
settings.isGridVisible.toggle()
} label: {
Image(systemName: "square.grid.3x3")
.font(.body)
.foregroundStyle(settings.isGridVisible ? .yellow : .white)
.padding(Design.Spacing.small)
.background(.ultraThinMaterial, in: Circle())
}
.accessibilityLabel(String(localized: "Toggle grid"))
.accessibilityValue(settings.isGridVisible ? "On" : "Off")
// Settings button
Button {
showSettings = true
} label: {
Image(systemName: "gearshape.fill")
.font(.body)
.foregroundStyle(.white)
.padding(Design.Spacing.small)
.background(.ultraThinMaterial, in: Circle())
}
.accessibilityLabel(String(localized: "Settings"))
}
}
// MARK: - Center Stage
private func checkCenterStageAvailability() {
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
isCenterStageAvailable = false
return
}
isCenterStageAvailable = device.activeFormat.isCenterStageSupported
isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled
}
private func toggleCenterStage() {
AVCaptureDevice.centerStageControlMode = .app
AVCaptureDevice.isCenterStageEnabled.toggle()
isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled
}
// MARK: - Post Capture View
@ViewBuilder
private var postCaptureView: some View {
if let image = capturedImage {
PostCapturePreviewView(
media: .photo(image),
isPremiumUnlocked: premiumManager.isPremiumUnlocked,
onRetake: { dismissPostCapture() },
onSave: { saveImage(image) },
onShare: {},
onDismiss: { dismissPostCapture() }
)
} else if let url = capturedVideoURL {
PostCapturePreviewView(
media: .video(url),
isPremiumUnlocked: premiumManager.isPremiumUnlocked,
onRetake: { dismissPostCapture() },
onSave: { saveVideo(url) },
onShare: {},
onDismiss: { dismissPostCapture() }
) )
} }
} }
// MARK: - Save Capture // MARK: - Toast View
private func saveCapture() { private func toastView(message: String) -> some View {
if let image = capturedImage { VStack {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) Spacer()
Text(message)
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(.ultraThinMaterial, in: Capsule())
.padding(.bottom, Design.Spacing.xxxLarge)
.transition(.move(edge: .bottom).combined(with: .opacity))
.animation(.spring(duration: Design.Animation.quick), value: message)
} }
.accessibilityLabel(message)
}
// MARK: - Capture Handlers
private func handleImageCaptured(_ image: UIImage) {
capturedImage = image
if settings.isAutoSaveEnabled {
saveImage(image)
showToast(String(localized: "Saved to Photos"))
}
showPostCapturePreview = true
}
private func handleVideoCaptured(_ url: URL) {
capturedVideoURL = url
if settings.isAutoSaveEnabled {
saveVideo(url)
showToast(String(localized: "Saved to Photos"))
}
showPostCapturePreview = true
}
private func dismissPostCapture() {
showPostCapturePreview = false
capturedImage = nil capturedImage = nil
capturedVideoURL = nil capturedVideoURL = nil
} }
private func saveImage(_ image: UIImage) {
guard let data = image.jpegData(compressionQuality: 0.9) else { return }
PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: data, options: nil)
}
}
private func saveVideo(_ url: URL) {
PHPhotoLibrary.shared().performChanges {
PHAssetCreationRequest.forAsset().addResource(with: .video, fileURL: url, options: nil)
}
}
private func showToast(_ message: String) {
toastMessage = message
Task {
try? await Task.sleep(for: .seconds(2))
if toastMessage == message {
toastMessage = nil
}
}
}
} }
#Preview { #Preview {

View File

@ -2,19 +2,35 @@ import SwiftUI
import AVKit import AVKit
import Bedrock import Bedrock
// MARK: - Captured Media Type
/// Represents captured media for preview
enum CapturedMedia: Equatable {
case photo(UIImage)
case video(URL)
case boomerang(URL)
var isVideo: Bool {
switch self {
case .photo: return false
case .video, .boomerang: return true
}
}
}
// MARK: - Post Capture Preview View // MARK: - Post Capture Preview View
/// Full-screen preview shown after photo/video capture /// Full-screen preview shown after photo/video capture
struct PostCapturePreviewView: View { struct PostCapturePreviewView: View {
let capturedImage: UIImage? let media: CapturedMedia
let capturedVideoURL: URL? let isPremiumUnlocked: Bool
let isAutoSaveEnabled: Bool
let onRetake: () -> Void let onRetake: () -> Void
let onSave: () -> Void let onSave: () -> Void
let onShare: () -> Void
let onDismiss: () -> Void
@State private var showEditSheet = false
@State private var player: AVPlayer? @State private var player: AVPlayer?
@State private var showShareSheet = false
@State private var toastMessage: String?
var body: some View { var body: some View {
ZStack { ZStack {
@ -34,27 +50,18 @@ struct PostCapturePreviewView: View {
// Bottom toolbar // Bottom toolbar
bottomToolbar bottomToolbar
} }
// Toast notification
if let message = toastMessage {
toastView(message: message)
}
} }
.onAppear { .onAppear {
setupVideoPlayerIfNeeded() setupVideoPlayerIfNeeded()
if isAutoSaveEnabled {
autoSave()
}
} }
.onDisappear { .onDisappear {
player?.pause() player?.pause()
} }
.sheet(isPresented: $showShareSheet) { .sheet(isPresented: $showEditSheet) {
if let image = capturedImage { PostCaptureEditView(
ShareSheet(items: [image]) media: media,
} else if let url = capturedVideoURL { isPremiumUnlocked: isPremiumUnlocked
ShareSheet(items: [url]) )
}
} }
} }
@ -62,20 +69,28 @@ struct PostCapturePreviewView: View {
@ViewBuilder @ViewBuilder
private var mediaPreview: some View { private var mediaPreview: some View {
if let image = capturedImage { switch media {
case .photo(let image):
Image(uiImage: image) Image(uiImage: image)
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.accessibilityLabel(String(localized: "Captured photo")) .accessibilityLabel(String(localized: "Captured photo"))
} else if let _ = capturedVideoURL, let player {
VideoPlayer(player: player) case .video(let url), .boomerang(let url):
.onAppear { if let player {
player.play() VideoPlayer(player: player)
} .onAppear {
.accessibilityLabel(String(localized: "Captured video")) player.play()
} else { }
ProgressView() .accessibilityLabel(
.tint(.white) media == .boomerang(url)
? String(localized: "Captured boomerang")
: String(localized: "Captured video")
)
} else {
ProgressView()
.tint(.white)
}
} }
} }
@ -84,7 +99,7 @@ struct PostCapturePreviewView: View {
private var topBar: some View { private var topBar: some View {
HStack { HStack {
Button { Button {
onRetake() onDismiss()
} label: { } label: {
Image(systemName: "xmark") Image(systemName: "xmark")
.font(.title2) .font(.title2)
@ -113,25 +128,20 @@ struct PostCapturePreviewView: View {
Spacer() Spacer()
// Save button (if not auto-saved) // Edit button
if !isAutoSaveEnabled { ToolbarButton(
ToolbarButton( title: String(localized: "Edit"),
title: String(localized: "Save"), systemImage: "slider.horizontal.3",
systemImage: "square.and.arrow.down", action: { showEditSheet = true }
action: { )
onSave()
showToast(String(localized: "Saved to Photos")) Spacer()
}
)
Spacer()
}
// Share button // Share button
ToolbarButton( ToolbarButton(
title: String(localized: "Share"), title: String(localized: "Share"),
systemImage: "square.and.arrow.up", systemImage: "square.and.arrow.up",
action: { showShareSheet = true } action: onShare
) )
} }
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
@ -139,51 +149,26 @@ struct PostCapturePreviewView: View {
.background(.ultraThinMaterial) .background(.ultraThinMaterial)
} }
// MARK: - Toast View
private func toastView(message: String) -> some View {
VStack {
Spacer()
Text(message)
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(.ultraThinMaterial, in: .capsule)
.padding(.bottom, 100)
}
.transition(.move(edge: .bottom).combined(with: .opacity))
.animation(.easeInOut, value: toastMessage)
}
// MARK: - Video Setup // MARK: - Video Setup
private func setupVideoPlayerIfNeeded() { private func setupVideoPlayerIfNeeded() {
guard let url = capturedVideoURL else { return } switch media {
player = AVPlayer(url: url) case .photo:
} break
case .video(let url):
// MARK: - Auto Save player = AVPlayer(url: url)
case .boomerang(let url):
private func autoSave() { let player = AVPlayer(url: url)
if let image = capturedImage { // Loop boomerang videos
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) NotificationCenter.default.addObserver(
showToast(String(localized: "Saved to Photos")) forName: .AVPlayerItemDidPlayToEndTime,
} object: player.currentItem,
// Video saving would go here queue: .main
} ) { _ in
player.seek(to: .zero)
private func showToast(_ message: String) { player.play()
withAnimation {
toastMessage = message
}
Task {
try? await Task.sleep(for: .seconds(2))
withAnimation {
toastMessage = nil
} }
self.player = player
} }
} }
} }
@ -209,24 +194,153 @@ private struct ToolbarButton: View {
} }
} }
// MARK: - Share Sheet // MARK: - Post Capture Edit View
struct ShareSheet: UIViewControllerRepresentable { /// Lightweight editor for captured media
let items: [Any] struct PostCaptureEditView: View {
let media: CapturedMedia
let isPremiumUnlocked: Bool
func makeUIViewController(context: Context) -> UIActivityViewController { @Environment(\.dismiss) private var dismiss
UIActivityViewController(activityItems: items, applicationActivities: nil)
@State private var smoothingIntensity: Double = 0.5
@State private var glowIntensity: Double = 0.3
var body: some View {
NavigationStack {
VStack(spacing: Design.Spacing.large) {
// Preview (simplified for now)
mediaPreview
.frame(maxHeight: .infinity)
// Edit controls
VStack(spacing: Design.Spacing.medium) {
// Smoothing slider
EditSlider(
title: String(localized: "Smoothing"),
value: $smoothingIntensity,
systemImage: "wand.and.stars"
)
// Glow effect slider
EditSlider(
title: String(localized: "Ring Glow"),
value: $glowIntensity,
systemImage: "light.max"
)
// Premium tools placeholder
if !isPremiumUnlocked {
premiumToolsTeaser
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.bottom, Design.Spacing.large)
}
.background(Color.Surface.overlay)
.navigationTitle(String(localized: "Edit"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button(String(localized: "Cancel")) {
dismiss()
}
.foregroundStyle(.white)
}
ToolbarItem(placement: .topBarTrailing) {
Button(String(localized: "Done")) {
// Apply edits and dismiss
dismiss()
}
.foregroundStyle(Color.Accent.primary)
.bold()
}
}
}
} }
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} @ViewBuilder
private var mediaPreview: some View {
switch media {
case .photo(let image):
Image(uiImage: image)
.resizable()
.scaledToFit()
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
.padding(Design.Spacing.medium)
case .video, .boomerang:
// Video editing would show a frame or thumbnail
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(.gray.opacity(Design.Opacity.medium))
.overlay {
Image(systemName: "video.fill")
.font(.largeTitle)
.foregroundStyle(.white)
}
.padding(Design.Spacing.medium)
}
}
private var premiumToolsTeaser: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "crown.fill")
.foregroundStyle(Color.Status.warning)
Text(String(localized: "Unlock filters, AI remove, and more with Pro"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
.padding(Design.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(Color.Accent.primary.opacity(Design.Opacity.subtle))
)
}
}
// MARK: - Edit Slider
private struct EditSlider: View {
let title: String
@Binding var value: Double
let systemImage: String
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
HStack {
Image(systemName: systemImage)
.foregroundStyle(.white.opacity(Design.Opacity.strong))
Text(title)
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
Spacer()
Text("\(Int(value * 100))%")
.font(.system(size: Design.BaseFontSize.caption, design: .rounded))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Slider(value: $value, in: 0...1, step: 0.05)
.tint(Color.Accent.primary)
}
}
} }
#Preview { #Preview {
PostCapturePreviewView( PostCapturePreviewView(
capturedImage: UIImage(systemName: "photo"), media: .photo(UIImage(systemName: "photo")!),
capturedVideoURL: nil, isPremiumUnlocked: false,
isAutoSaveEnabled: false,
onRetake: {}, onRetake: {},
onSave: {} onSave: {},
onShare: {},
onDismiss: {}
) )
} }

View File

@ -372,8 +372,8 @@ struct LicensesView: View {
licenseCard( licenseCard(
name: "MijickCamera", name: "MijickCamera",
url: "https://github.com/Mijick/Camera", url: "https://github.com/Mijick/Camera",
license: "Apache 2.0 License", license: "Apache License 2.0",
description: "Camera framework for SwiftUI. Created by Tomasz Kurylik at Mijick." description: "Camera made simple. The ultimate camera library that significantly reduces implementation time and effort."
) )
// RevenueCat // RevenueCat

View File

@ -5,6 +5,10 @@
"comment" : "The value of the ring size slider, displayed in parentheses.", "comment" : "The value of the ring size slider, displayed in parentheses.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"%lld%%" : {
"comment" : "A text label displaying the current brightness percentage.",
"isCommentAutoGenerated" : true
},
"%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\".",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -61,6 +65,10 @@
"comment" : "The text for a button that dismisses the current view.", "comment" : "The text for a button that dismisses the current view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Captured boomerang" : {
"comment" : "A label describing a captured boomerang.",
"isCommentAutoGenerated" : true
},
"Captured photo" : { "Captured photo" : {
"comment" : "A label describing a captured photo.", "comment" : "A label describing a captured photo.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -69,6 +77,10 @@
"comment" : "A label describing a captured video.", "comment" : "A label describing a captured video.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Center Stage" : {
"comment" : "A button that toggles whether the user is centered in the video feed.",
"isCommentAutoGenerated" : true
},
"Close preview" : { "Close preview" : {
"comment" : "A button label that closes the preview screen.", "comment" : "A button label that closes the preview screen.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -101,6 +113,10 @@
"comment" : "The text for a button that dismisses a view. In this case, it dismisses the settings view.", "comment" : "The text for a button that dismisses a view. In this case, it dismisses the settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Edit" : {
"comment" : "Label for the button that allows the user to edit their captured photo or video.",
"isCommentAutoGenerated" : true
},
"Front Flash" : { "Front Flash" : {
"comment" : "Title of a toggle in the Settings view that controls whether the front flash is enabled.", "comment" : "Title of a toggle in the Settings view that controls whether the front flash is enabled.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -121,6 +137,10 @@
"comment" : "Name of a ring light color preset.", "comment" : "Name of a ring light color preset.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Keeps you centered in frame" : {
"comment" : "A hint that explains the purpose of the \"Center Stage\" button.",
"isCommentAutoGenerated" : true
},
"Last synced %@" : { "Last synced %@" : {
}, },
@ -140,6 +160,10 @@
"comment" : "The accessibility value for the grid toggle when it is off.", "comment" : "The accessibility value for the grid toggle when it is off.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"On" : {
"comment" : "A label describing a setting that is currently enabled.",
"isCommentAutoGenerated" : true
},
"Open Source Licenses" : { "Open Source Licenses" : {
"comment" : "A heading displayed above a list of open source licenses used in the app.", "comment" : "A heading displayed above a list of open source licenses used in the app.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -175,6 +199,10 @@
"comment" : "Title for a button that allows the user to retake a captured photo or video.", "comment" : "Title for a button that allows the user to retake a captured photo or video.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Ring Glow" : {
"comment" : "Title of a slider that controls the intensity of the ring glow effect in the captured media.",
"isCommentAutoGenerated" : true
},
"Ring size" : { "Ring size" : {
"comment" : "An accessibility label for the ring size slider in the settings view.", "comment" : "An accessibility label for the ring size slider in the settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -183,10 +211,6 @@
"comment" : "The label for the ring size slider in the settings view.", "comment" : "The label for the ring size slider in the settings view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Save" : {
"comment" : "Title for a button that saves the currently captured photo or video to the user's photo library.",
"isCommentAutoGenerated" : true
},
"Saved to Photos" : { "Saved to Photos" : {
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.", "comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -226,6 +250,9 @@
"Skin Smoothing" : { "Skin Smoothing" : {
"comment" : "A toggle that enables or disables real-time skin smoothing.", "comment" : "A toggle that enables or disables real-time skin smoothing.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Smoothing" : {
}, },
"Soft Pink" : { "Soft Pink" : {
"comment" : "Name of a ring light color preset.", "comment" : "Name of a ring light color preset.",
@ -266,6 +293,10 @@
"comment" : "A description of the third-party libraries used in this app.", "comment" : "A description of the third-party libraries used in this app.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Toggle grid" : {
"comment" : "A button that toggles the visibility of the grid in the camera view.",
"isCommentAutoGenerated" : true
},
"True Mirror" : { "True Mirror" : {
"comment" : "Title of a toggle in the settings view that allows the user to flip the camera preview.", "comment" : "Title of a toggle in the settings view that allows the user to flip the camera preview.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -274,6 +305,10 @@
"comment" : "Description of a benefit that comes with the Pro subscription, specifically related to the boomerang tool.", "comment" : "Description of a benefit that comes with the Pro subscription, specifically related to the boomerang tool.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Unlock filters, AI remove, and more with Pro" : {
"comment" : "A teaser text that appears below the capture edit view, promoting a premium feature.",
"isCommentAutoGenerated" : true
},
"Unlock premium colors, video, and more" : { "Unlock premium colors, video, and more" : {
"comment" : "A description of the benefits of upgrading to the Pro version of the app.", "comment" : "A description of the benefits of upgrading to the Pro version of the app.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true