FullScreenCarouselView.swift (8370B)
1 // 2 // FullScreenCarouselView.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-03-23. 6 // 7 8 import SwiftUI 9 10 struct FullScreenCarouselView<Content: View>: View { 11 @ObservedObject var video_coordinator: DamusVideoCoordinator 12 let urls: [MediaUrl] 13 14 @Environment(\.presentationMode) var presentationMode 15 16 @State var showMenu = true 17 @State private var imageDict: [URL: UIImage] = [:] 18 let settings: UserSettingsStore 19 @ObservedObject var carouselSelection: CarouselSelection 20 let content: (() -> Content)? 21 22 init(video_coordinator: DamusVideoCoordinator, urls: [MediaUrl], showMenu: Bool = true, settings: UserSettingsStore, selectedIndex: Binding<Int>, @ViewBuilder content: @escaping () -> Content) { 23 self.video_coordinator = video_coordinator 24 self.urls = urls 25 self._showMenu = State(initialValue: showMenu) 26 self.settings = settings 27 self._carouselSelection = ObservedObject(initialValue: CarouselSelection(index: selectedIndex.wrappedValue)) 28 self.content = content 29 } 30 31 init(video_coordinator: DamusVideoCoordinator, urls: [MediaUrl], showMenu: Bool = true, settings: UserSettingsStore, selectedIndex: Binding<Int>) { 32 self.video_coordinator = video_coordinator 33 self.urls = urls 34 self._showMenu = State(initialValue: showMenu) 35 self.settings = settings 36 self._carouselSelection = ObservedObject(initialValue: CarouselSelection(index: selectedIndex.wrappedValue)) 37 self.content = nil 38 } 39 40 var background: some ShapeStyle { 41 if case .video = urls[safe: carouselSelection.index] { 42 return AnyShapeStyle(Color.black) 43 } 44 else { 45 return AnyShapeStyle(.regularMaterial) 46 } 47 } 48 49 var background_color: UIColor { 50 return .black 51 } 52 53 var body: some View { 54 ZStack { 55 Color(self.background_color) 56 .ignoresSafeArea() 57 58 TabView(selection: $carouselSelection.index) { 59 ForEach(urls.indices, id: \.self) { index in 60 VStack { 61 if case .video = urls[safe: index] { 62 ImageContainerView( 63 video_coordinator: video_coordinator, 64 url: urls[index], 65 settings: settings, 66 imageDict: $imageDict 67 ) 68 .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: { 69 presentationMode.wrappedValue.dismiss() 70 })) 71 .ignoresSafeArea() 72 } 73 else { 74 ZoomableScrollView { 75 ImageContainerView(video_coordinator: video_coordinator, url: urls[index], settings: settings, imageDict: $imageDict) 76 .aspectRatio(contentMode: .fit) 77 .padding(.top, Theme.safeAreaInsets?.top) 78 .padding(.bottom, Theme.safeAreaInsets?.bottom) 79 } 80 .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: { 81 presentationMode.wrappedValue.dismiss() 82 })) 83 .ignoresSafeArea() 84 } 85 }.tag(index) 86 } 87 } 88 .ignoresSafeArea() 89 .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) 90 .gesture(TapGesture(count: 2).onEnded { 91 // Prevents menu from hiding on double tap 92 }) 93 .gesture(TapGesture(count: 1).onEnded { 94 showMenu.toggle() 95 }) 96 .overlay( 97 GeometryReader { geo in 98 VStack { 99 if showMenu { 100 HStack { 101 Button(action: { 102 presentationMode.wrappedValue.dismiss() 103 }, label: { 104 Image(systemName: "xmark") 105 .frame(width: 30, height: 30) 106 }) 107 .buttonStyle(PlayerCircleButtonStyle()) 108 109 Spacer() 110 111 if let url = urls[safe: carouselSelection.index], 112 let image = imageDict[url.url] { 113 114 ShareLink(item: Image(uiImage: image), 115 preview: SharePreview(NSLocalizedString("Shared Picture", 116 comment: "Label for the preview of the image being picture"), 117 image: Image(uiImage: image))) { 118 Image(systemName: "ellipsis") 119 .frame(width: 30, height: 30) 120 } 121 .buttonStyle(PlayerCircleButtonStyle()) 122 } 123 } 124 .padding() 125 126 Spacer() 127 128 VStack { 129 if urls.count > 1 { 130 PageControlView(currentPage: $carouselSelection.index, numberOfPages: urls.count) 131 .frame(maxWidth: 0, maxHeight: 0) 132 .padding(.top, 5) 133 } 134 135 if let focused_video = video_coordinator.focused_video { 136 DamusVideoControlsView(video: focused_video) 137 } 138 139 self.content?() 140 } 141 .padding(.top, 5) 142 .background(Color.black.opacity(0.7)) 143 } 144 } 145 .animation(.easeInOut, value: showMenu) 146 .padding(.bottom, geo.safeAreaInsets.bottom == 0 ? 12 : 0) 147 } 148 ) 149 } 150 } 151 } 152 153 fileprivate struct FullScreenCarouselPreviewView<Content: View>: View { 154 @State var selectedIndex: Int = 0 155 let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!) 156 let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!) 157 let custom_content: (() -> Content)? 158 159 init(content: (() -> Content)? = nil) { 160 self.custom_content = content 161 } 162 163 var body: some View { 164 FullScreenCarouselView(video_coordinator: test_damus_state.video, urls: [test_video_url, url], settings: test_damus_state.settings, selectedIndex: $selectedIndex) { 165 self.custom_content?() 166 } 167 .environmentObject(OrientationTracker()) 168 } 169 } 170 171 struct FullScreenCarouselView_Previews: PreviewProvider { 172 static var previews: some View { 173 Group { 174 FullScreenCarouselPreviewView<AnyView>() 175 .previewDisplayName("No custom content on overlay") 176 177 FullScreenCarouselPreviewView(content: { 178 HStack { 179 Spacer() 180 181 Text(verbatim: "Some content") 182 .padding() 183 .foregroundColor(.white) 184 185 Spacer() 186 }.background(.ultraThinMaterial) 187 }) 188 .previewDisplayName("Custom content on overlay") 189 } 190 } 191 } 192 193 /// Class to define object for monitoring selectedIndex and updating mutlples views 194 final class CarouselSelection: ObservableObject { 195 @Published var index: Int 196 init(index: Int) { 197 self.index = index 198 } 199 }