Skip to content

Commit 49860d7

Browse files
committed
camera: add ability to preview media taken with camera
1 parent 4413ec0 commit 49860d7

File tree

5 files changed

+338
-8
lines changed

5 files changed

+338
-8
lines changed

damus.xcodeproj/project.pbxproj

+8-2
Original file line numberDiff line numberDiff line change
@@ -433,11 +433,12 @@
433433
B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; };
434434
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */; };
435435
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
436-
BA0F0A6F2B36207E001641B2 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */; };
437436
BA10192F2B449556009C57DA /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA10192E2B449556009C57DA /* CameraPreview.swift */; };
438437
B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
439438
B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
440439
B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */; };
440+
BA15BB6B2B7833660045B913 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA15BB6A2B7833660045B913 /* CameraMediaView.swift */; };
441+
BA15BB6D2B78336D0045B913 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA15BB6C2B78336D0045B913 /* CameraView.swift */; };
441442
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
442443
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
443444
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -1355,10 +1356,11 @@
13551356
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
13561357
B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItemTests.swift; sourceTree = "<group>"; usesTabs = 0; };
13571358
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
1358-
BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = "<group>"; };
13591359
BA10192E2B449556009C57DA /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
13601360
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItem.swift; sourceTree = "<group>"; usesTabs = 0; };
13611361
B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusDuration.swift; sourceTree = "<group>"; usesTabs = 0; };
1362+
BA15BB6A2B7833660045B913 /* CameraMediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = "<group>"; };
1363+
BA15BB6C2B78336D0045B913 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
13621364
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
13631365
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
13641366
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
@@ -2692,8 +2694,10 @@
26922694
BA3759952ABCCF360018D73B /* Camera */ = {
26932695
isa = PBXGroup;
26942696
children = (
2697+
BA15BB6A2B7833660045B913 /* CameraMediaView.swift */,
26952698
BA3759962ABCCF360018D73B /* CameraPreview.swift */,
26962699
E02429942B7E97740088B16C /* CameraController.swift */,
2700+
BA15BB6C2B78336D0045B913 /* CameraView.swift */,
26972701
);
26982702
path = Camera;
26992703
sourceTree = "<group>";
@@ -3045,6 +3049,7 @@
30453049
4C4793042A993DC000489948 /* midl.c in Sources */,
30463050
0E8A4BB72AE4359200065E81 /* NostrFilter+Hashable.swift in Sources */,
30473051
4C4793012A993CDA00489948 /* mdb.c in Sources */,
3052+
BA15BB6D2B78336D0045B913 /* CameraView.swift in Sources */,
30483053
4CE9FBBA2A6B3C63007E485C /* nostrdb.c in Sources */,
30493054
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */,
30503055
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
@@ -3131,6 +3136,7 @@
31313136
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
31323137
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
31333138
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
3139+
BA15BB6B2B7833660045B913 /* CameraMediaView.swift in Sources */,
31343140
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
31353141
B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */,
31363142
4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */,

damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme

-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
savedToolIdentifier = ""
7878
useCustomWorkingDirectory = "NO"
7979
debugDocumentVersioning = "YES"
80-
askForAppToLaunch = "Yes"
8180
launchAutomaticallySubstyle = "2">
8281
<BuildableProductRunnable
8382
runnableDebuggingMode = "0">
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// MediaViewer.swift
3+
// damus
4+
//
5+
// Created by Suhail Saqan on 12/22/23.
6+
//
7+
8+
import SwiftUI
9+
import Kingfisher
10+
11+
// MARK: - Camera Media Viewer
12+
struct CameraMediaView: View {
13+
let video_controller: VideoController
14+
let urls: [MediaUrl]
15+
16+
@Environment(\.presentationMode) var presentationMode
17+
18+
@State private var selectedIndex = 0
19+
@State var showMenu = true
20+
21+
let settings: UserSettingsStore
22+
23+
var tabViewIndicator: some View {
24+
HStack(spacing: 10) {
25+
ForEach(urls.indices, id: \.self) { index in
26+
Capsule()
27+
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
28+
.frame(width: 7, height: 7)
29+
.onTapGesture {
30+
selectedIndex = index
31+
}
32+
}
33+
}
34+
.padding()
35+
.background(.regularMaterial)
36+
.clipShape(Capsule())
37+
}
38+
39+
var body: some View {
40+
ZStack {
41+
Color(.systemBackground)
42+
.ignoresSafeArea()
43+
44+
TabView(selection: $selectedIndex) {
45+
ForEach(urls.indices, id: \.self) { index in
46+
ZoomableScrollView {
47+
ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings)
48+
.aspectRatio(contentMode: .fit)
49+
.padding(.top, Theme.safeAreaInsets?.top)
50+
.padding(.bottom, Theme.safeAreaInsets?.bottom)
51+
}
52+
.ignoresSafeArea()
53+
.tag(index)
54+
}
55+
}
56+
.ignoresSafeArea()
57+
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
58+
.gesture(TapGesture(count: 2).onEnded {
59+
// Prevents menu from hiding on double tap
60+
})
61+
.gesture(TapGesture(count: 1).onEnded {
62+
showMenu.toggle()
63+
})
64+
.overlay(
65+
GeometryReader { geo in
66+
VStack {
67+
if showMenu {
68+
NavDismissBarView()
69+
Spacer()
70+
71+
if (urls.count > 1) {
72+
tabViewIndicator
73+
}
74+
}
75+
}
76+
.animation(.easeInOut, value: showMenu)
77+
.padding(.bottom, geo.safeAreaInsets.bottom == 0 ? 12 : 0)
78+
}
79+
)
80+
}
81+
}
82+
}
83+
84+
struct CameraMediaView_Previews: PreviewProvider {
85+
static var previews: some View {
86+
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
87+
CameraMediaView(video_controller: test_damus_state.video, urls: [url], settings: test_damus_state.settings)
88+
}
89+
}

damus/Views/Camera/CameraView.swift

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//
2+
// CameraView.swift
3+
// damus
4+
//
5+
// Created by Suhail Saqan on 8/5/23.
6+
//
7+
8+
import SwiftUI
9+
import Combine
10+
import AVFoundation
11+
12+
struct CameraView: View {
13+
let damus_state: DamusState
14+
let action: (([MediaItem]) -> Void)
15+
16+
@Environment(\.presentationMode) var presentationMode
17+
18+
@StateObject var model: CameraModel
19+
20+
@State var currentZoomFactor: CGFloat = 1.0
21+
22+
public init(damus_state: DamusState, action: @escaping (([MediaItem]) -> Void)) {
23+
self.damus_state = damus_state
24+
self.action = action
25+
_model = StateObject(wrappedValue: CameraModel())
26+
}
27+
28+
var captureButton: some View {
29+
Button {
30+
if model.isRecording {
31+
withAnimation {
32+
model.stopRecording()
33+
}
34+
} else {
35+
withAnimation {
36+
model.capturePhoto()
37+
}
38+
}
39+
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
40+
} label: {
41+
ZStack {
42+
Circle()
43+
.fill( model.isRecording ? .red : DamusColors.black)
44+
.frame(width: model.isRecording ? 85 : 65, height: model.isRecording ? 85 : 65, alignment: .center)
45+
46+
Circle()
47+
.stroke( model.isRecording ? .red : DamusColors.white, lineWidth: 4)
48+
.frame(width: model.isRecording ? 95 : 75, height: model.isRecording ? 95 : 75, alignment: .center)
49+
}
50+
.frame(alignment: .center)
51+
}
52+
.simultaneousGesture(
53+
LongPressGesture(minimumDuration: 0.5).onEnded({ value in
54+
if (!model.isCameraButtonDisabled) {
55+
withAnimation {
56+
model.startRecording()
57+
model.captureMode = .video
58+
}
59+
}
60+
})
61+
)
62+
.buttonStyle(.plain)
63+
}
64+
65+
var capturedPhotoThumbnail: some View {
66+
ZStack {
67+
if model.thumbnail != nil {
68+
Image(uiImage: model.thumbnail.thumbnailImage!)
69+
.resizable()
70+
.aspectRatio(contentMode: .fill)
71+
.frame(width: 60, height: 60)
72+
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
73+
}
74+
if model.isPhotoProcessing {
75+
ProgressView()
76+
.progressViewStyle(CircularProgressViewStyle(tint: DamusColors.white))
77+
}
78+
}
79+
}
80+
81+
var closeButton: some View {
82+
Button {
83+
presentationMode.wrappedValue.dismiss()
84+
model.stop()
85+
} label: {
86+
HStack {
87+
Image(systemName: "xmark")
88+
.font(.system(size: 24))
89+
}
90+
.frame(minWidth: 40, minHeight: 40)
91+
}
92+
.accentColor(DamusColors.white)
93+
}
94+
95+
var flipCameraButton: some View {
96+
Button(action: {
97+
model.flipCamera()
98+
}, label: {
99+
HStack {
100+
Image(systemName: "camera.rotate.fill")
101+
.font(.system(size: 20))
102+
}
103+
.frame(minWidth: 40, minHeight: 40)
104+
})
105+
.accentColor(DamusColors.white)
106+
}
107+
108+
var toggleFlashButton: some View {
109+
Button(action: {
110+
model.switchFlash()
111+
}, label: {
112+
HStack {
113+
Image(systemName: model.isFlashOn ? "bolt.fill" : "bolt.slash.fill")
114+
.font(.system(size: 20))
115+
}
116+
.frame(minWidth: 40, minHeight: 40)
117+
})
118+
.accentColor(model.isFlashOn ? .yellow : DamusColors.white)
119+
}
120+
121+
var body: some View {
122+
NavigationView {
123+
GeometryReader { reader in
124+
ZStack {
125+
DamusColors.black.edgesIgnoringSafeArea(.all)
126+
127+
CameraPreview(session: model.session)
128+
.padding(.bottom, 175)
129+
.edgesIgnoringSafeArea(.all)
130+
.gesture(
131+
DragGesture().onChanged({ (val) in
132+
if abs(val.translation.height) > abs(val.translation.width) {
133+
let percentage: CGFloat = -(val.translation.height / reader.size.height)
134+
let calc = currentZoomFactor + percentage
135+
let zoomFactor: CGFloat = min(max(calc, 1), 5)
136+
137+
currentZoomFactor = zoomFactor
138+
model.zoom(with: zoomFactor)
139+
}
140+
})
141+
)
142+
.onAppear {
143+
model.configure()
144+
}
145+
.alert(isPresented: $model.showAlertError, content: {
146+
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
147+
model.alertError.primaryAction?()
148+
}))
149+
})
150+
.overlay(
151+
Group {
152+
if model.willCapturePhoto {
153+
Color.black
154+
}
155+
}
156+
)
157+
158+
VStack {
159+
if !model.isRecording {
160+
HStack {
161+
closeButton
162+
163+
Spacer()
164+
165+
HStack {
166+
flipCameraButton
167+
toggleFlashButton
168+
}
169+
}
170+
.padding(.horizontal, 20)
171+
}
172+
173+
Spacer()
174+
175+
HStack(alignment: .center) {
176+
if !model.mediaItems.isEmpty {
177+
NavigationLink(destination: CameraMediaView(video_controller: damus_state.video, urls: model.mediaItems.map { mediaItem in
178+
switch mediaItem.type {
179+
case .image:
180+
return .image(mediaItem.url)
181+
case .video:
182+
return .video(mediaItem.url)
183+
}
184+
}, settings: damus_state.settings)
185+
.navigationBarBackButtonHidden(true)
186+
) {
187+
capturedPhotoThumbnail
188+
}
189+
.frame(width: 100, alignment: .leading)
190+
}
191+
192+
Spacer()
193+
194+
captureButton
195+
196+
Spacer()
197+
198+
if !model.mediaItems.isEmpty {
199+
Button(action: {
200+
action(model.mediaItems)
201+
presentationMode.wrappedValue.dismiss()
202+
model.stop()
203+
}) {
204+
Text("Upload")
205+
.frame(width: 100, height: 40, alignment: .center)
206+
.foregroundColor(DamusColors.white)
207+
.overlay {
208+
RoundedRectangle(cornerRadius: 24)
209+
.stroke(DamusColors.white, lineWidth: 2)
210+
}
211+
}
212+
}
213+
}
214+
.frame(height: 100)
215+
.padding([.horizontal, .vertical], 20)
216+
}
217+
}
218+
}
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)