Compare commits

...

10 Commits

Author SHA1 Message Date
e824119dcb Signed-off-by: Matt Bruce <mbrucedogs@gmail.com> 2026-01-04 10:28:19 -06:00
e4cb7fc9da Fix: Default to front camera and remove clipShape
- Set .setCameraPosition(.front) for selfie mode
- Remove clipShape so MijickCamera's controls aren't clipped
- Reduced max ring size to 20% to leave room for controls
- Fixed typo in capturedImage variable name
2026-01-02 16:54:59 -06:00
9250a44a65 Use MijickCamera's default UI with ring light overlay
- Remove custom RingLightCameraScreen
- Use MijickCamera's built-in camera UI (capture, flip, etc.)
- Ring light is now the background with MCamera padded inside
- Only overlay a Settings button on top
- Much simpler and uses the package's tested UI
2026-01-02 16:51:29 -06:00
7e5c4a021f Use native camera aspect ratio instead of forced 4:3
- Let MijickCamera determine the preview aspect ratio
- Ring light padding on horizontal sides (left/right)
- Smaller vertical padding between controls and camera
- Camera preview fills available space naturally
2026-01-02 16:47:59 -06:00
22586d6693 Fix camera layout: proper aspect ratio, centering, and visible ring light
- Camera preview now uses 4:3 aspect ratio (matches photo capture)
- Camera is centered both horizontally and vertically
- Ring light background properly visible around camera
- Controls positioned correctly with safe area handling
- Grid overlay now clips to camera bounds
- Improved control bar layout with proper spacing
2026-01-02 16:46:08 -06:00
7bada93504 Add MijickCamera attribution to licenses 2026-01-02 16:38:25 -06:00
f38e0bf22c Integrate MijickCamera with ring light effect
- Add MijickCamera package for camera handling
- Create RingLightCameraScreen conforming to MCameraScreen protocol
- Ring light background fills screen with settings.lightColor
- Camera preview padded inward to create ring effect
- Custom capture button and controls overlay
- Grid overlay support
- Camera flip button using MijickCamera API
- Delete old CameraViewModel and CameraPreview (replaced by MijickCamera)
- Simplified PostCapturePreviewView for photo/video preview
2026-01-02 16:37:49 -06:00
72bac70ea1 Test: Minimal MCamera implementation to verify it works
Simple ContentView with just MCamera() and nothing else.
If this works, we can add the ring light back incrementally.
2026-01-02 16:32:09 -06:00
442f5d6e1d Remove MijickCamera from licenses (no longer used) 2026-01-02 16:27:15 -06:00
6e1ce6d262 Revert to custom camera implementation - MijickCamera not working
MijickCamera wasn't rendering the camera preview properly in our
view hierarchy. Reverting to our custom AVFoundation-based camera
implementation which was working correctly.

Reverted:
- Restored CameraViewModel.swift
- Restored CameraPreview.swift
- Restored ContentView.swift to pre-MijickCamera version
- Removed MijickCamera package dependency

Kept:
- Open Source Licenses section (now just shows RevenueCat)
- All other features and fixes

Our custom camera code handles:
- Camera preview with proper orientation
- Photo/video capture
- Front flash effect
- Center Stage support
- Manual rotation
- Zoom gestures
2026-01-02 16:26:41 -06:00
5 changed files with 184 additions and 516 deletions

View File

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

View File

@ -1,287 +1,104 @@
import SwiftUI
import MijickCamera
import Bedrock
import Photos
import AVFoundation
/// Main camera view with ring light effect using MijickCamera
struct ContentView: View {
@State private var settings = SettingsViewModel()
@State private var premiumManager = PremiumManager()
@State private var showPaywall = false
@State private var showSettings = false
@State private var toastMessage: String?
@State private var showPaywall = false
// Captured media for post-capture preview
// Post-capture state
@State private var capturedImage: UIImage?
@State private var capturedVideoURL: URL?
@State private var showPostCapturePreview = false
@State private var showPostCapture = false
// Center Stage support
@State private var isCenterStageAvailable = false
@State private var isCenterStageEnabled = false
/// Ring size clamped to reasonable max
private var effectiveRingSize: CGFloat {
let maxRing = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * 0.2
return min(settings.ringSize, maxRing)
}
var body: some View {
GeometryReader { geometry in
let maxRingSize = calculateMaxRingSize(for: geometry)
let effectiveRingSize = min(settings.ringSize, maxRingSize)
ZStack {
// Ring light background
settings.lightColor
.ignoresSafeArea()
// Use MCamera as the base, with ring light as border
// MijickCamera with default UI
MCamera()
.setCameraPosition(.front) // Default to front camera for selfies
.onImageCaptured { image, _ in
handleImageCaptured(image)
capturedImage = image
showPostCapture = true
}
.onVideoCaptured { url, _ in
handleVideoCaptured(url)
capturedVideoURL = url
showPostCapture = true
}
.ignoresSafeArea()
.overlay {
// Ring light border overlay
ringLightOverlay(ringSize: effectiveRingSize)
}
.overlay {
// Grid overlay
if settings.isGridVisible {
GridOverlay(isVisible: true)
.padding(effectiveRingSize)
.allowsHitTesting(false)
}
}
.overlay {
// Top controls
VStack {
topControlBar
.padding(.top, effectiveRingSize + Design.Spacing.small)
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
.startSession()
.padding(.horizontal, effectiveRingSize)
.padding(.top, effectiveRingSize)
.padding(.bottom, effectiveRingSize)
// Settings button overlay (top right corner of camera area)
VStack {
HStack {
Spacer()
Button {
showSettings = true
} label: {
Image(systemName: "gearshape.fill")
.font(.title3)
.foregroundStyle(.white)
.padding(Design.Spacing.medium)
.background(.ultraThinMaterial, in: Circle())
.shadow(radius: Design.Shadow.radiusSmall)
}
.accessibilityLabel("Settings")
}
.padding(.horizontal, effectiveRingSize + Design.Spacing.medium)
.padding(.top, effectiveRingSize + Design.Spacing.medium)
Spacer()
}
}
.onAppear {
checkCenterStageAvailability()
.ignoresSafeArea()
.sheet(isPresented: $showSettings) {
SettingsView(viewModel: settings, showPaywall: $showPaywall)
}
.sheet(isPresented: $showPaywall) {
ProPaywallView()
}
.sheet(isPresented: $showSettings) {
SettingsView(viewModel: settings, showPaywall: $showPaywall)
}
.fullScreenCover(isPresented: $showPostCapturePreview) {
postCaptureView
}
}
// MARK: - Max Ring Size Calculation
private func calculateMaxRingSize(for geometry: GeometryProxy) -> CGFloat {
min(geometry.size.width, geometry.size.height) / 4
}
// 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())
.fullScreenCover(isPresented: $showPostCapture) {
PostCapturePreviewView(
capturedImage: capturedImage,
capturedVideoURL: capturedVideoURL,
isAutoSaveEnabled: settings.isAutoSaveEnabled,
onRetake: {
capturedImage = nil
capturedVideoURL = nil
showPostCapture = false
},
onSave: {
saveCapture()
showPostCapture = false
}
.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
// MARK: - Save Capture
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 {
private func saveCapture() {
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() }
)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
// 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, 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
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 {

View File

@ -2,35 +2,19 @@ import SwiftUI
import AVKit
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
/// Full-screen preview shown after photo/video capture
struct PostCapturePreviewView: View {
let media: CapturedMedia
let isPremiumUnlocked: Bool
let capturedImage: UIImage?
let capturedVideoURL: URL?
let isAutoSaveEnabled: Bool
let onRetake: () -> Void
let onSave: () -> Void
let onShare: () -> Void
let onDismiss: () -> Void
@State private var showEditSheet = false
@State private var player: AVPlayer?
@State private var showShareSheet = false
@State private var toastMessage: String?
var body: some View {
ZStack {
@ -50,18 +34,27 @@ struct PostCapturePreviewView: View {
// Bottom toolbar
bottomToolbar
}
// Toast notification
if let message = toastMessage {
toastView(message: message)
}
}
.onAppear {
setupVideoPlayerIfNeeded()
if isAutoSaveEnabled {
autoSave()
}
}
.onDisappear {
player?.pause()
}
.sheet(isPresented: $showEditSheet) {
PostCaptureEditView(
media: media,
isPremiumUnlocked: isPremiumUnlocked
)
.sheet(isPresented: $showShareSheet) {
if let image = capturedImage {
ShareSheet(items: [image])
} else if let url = capturedVideoURL {
ShareSheet(items: [url])
}
}
}
@ -69,28 +62,20 @@ struct PostCapturePreviewView: View {
@ViewBuilder
private var mediaPreview: some View {
switch media {
case .photo(let image):
if let image = capturedImage {
Image(uiImage: image)
.resizable()
.scaledToFit()
.accessibilityLabel(String(localized: "Captured photo"))
case .video(let url), .boomerang(let url):
if let player {
VideoPlayer(player: player)
.onAppear {
player.play()
}
.accessibilityLabel(
media == .boomerang(url)
? String(localized: "Captured boomerang")
: String(localized: "Captured video")
)
} else {
ProgressView()
.tint(.white)
}
} else if let _ = capturedVideoURL, let player {
VideoPlayer(player: player)
.onAppear {
player.play()
}
.accessibilityLabel(String(localized: "Captured video"))
} else {
ProgressView()
.tint(.white)
}
}
@ -99,7 +84,7 @@ struct PostCapturePreviewView: View {
private var topBar: some View {
HStack {
Button {
onDismiss()
onRetake()
} label: {
Image(systemName: "xmark")
.font(.title2)
@ -128,20 +113,25 @@ struct PostCapturePreviewView: View {
Spacer()
// Edit button
ToolbarButton(
title: String(localized: "Edit"),
systemImage: "slider.horizontal.3",
action: { showEditSheet = true }
)
Spacer()
// Save button (if not auto-saved)
if !isAutoSaveEnabled {
ToolbarButton(
title: String(localized: "Save"),
systemImage: "square.and.arrow.down",
action: {
onSave()
showToast(String(localized: "Saved to Photos"))
}
)
Spacer()
}
// Share button
ToolbarButton(
title: String(localized: "Share"),
systemImage: "square.and.arrow.up",
action: onShare
action: { showShareSheet = true }
)
}
.padding(.horizontal, Design.Spacing.xxLarge)
@ -149,26 +139,51 @@ struct PostCapturePreviewView: View {
.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
private func setupVideoPlayerIfNeeded() {
switch media {
case .photo:
break
case .video(let url):
player = AVPlayer(url: url)
case .boomerang(let url):
let player = AVPlayer(url: url)
// Loop boomerang videos
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem,
queue: .main
) { _ in
player.seek(to: .zero)
player.play()
guard let url = capturedVideoURL else { return }
player = AVPlayer(url: url)
}
// MARK: - Auto Save
private func autoSave() {
if let image = capturedImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
showToast(String(localized: "Saved to Photos"))
}
// Video saving would go here
}
private func showToast(_ message: String) {
withAnimation {
toastMessage = message
}
Task {
try? await Task.sleep(for: .seconds(2))
withAnimation {
toastMessage = nil
}
self.player = player
}
}
}
@ -194,153 +209,24 @@ private struct ToolbarButton: View {
}
}
// MARK: - Post Capture Edit View
// MARK: - Share Sheet
/// Lightweight editor for captured media
struct PostCaptureEditView: View {
let media: CapturedMedia
let isPremiumUnlocked: Bool
struct ShareSheet: UIViewControllerRepresentable {
let items: [Any]
@Environment(\.dismiss) private var dismiss
@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 makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
@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)
}
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
#Preview {
PostCapturePreviewView(
media: .photo(UIImage(systemName: "photo")!),
isPremiumUnlocked: false,
capturedImage: UIImage(systemName: "photo"),
capturedVideoURL: nil,
isAutoSaveEnabled: false,
onRetake: {},
onSave: {},
onShare: {},
onDismiss: {}
onSave: {}
)
}

View File

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

View File

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