From 250efd975534347478502cbd0614038b1d07d096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 18 Mar 2024 22:21:28 +0000 Subject: [PATCH] Add event text to full-screen Carousel view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full screen carousel looks quite empty. When viewing videos, it looks like the video is being played full screen, when it is really not. Alas, SwiftUI/UIKit does not provide an API for programmatically bringing a video player full screen. The closest we can do is show the system native playback controls. This can cause confusion to users, and is not the best UX. To get around these limitations and improve UX, event information and content is added to the full screen carousel overlay, so that: - Users can see a piece of the post while they are browsing images and videos - Users can more clearly tell when the video is being displayed on the full screen carousel or on full screen - Users have a way to directly go to the thread view within the full screen carousel Changelog-Added: Add event content preview to the full screen carousel Signed-off-by: Daniel D’Aquino Link: 20240318222048.14226-4-daniel@daquino.me Signed-off-by: William Casarin --- damus/Components/ImageCarousel.swift | 26 ++++++++++++-- damus/Components/TruncatedText.swift | 7 +++- damus/Views/Events/TextEvent.swift | 3 ++ .../Views/Images/FullScreenCarouselView.swift | 25 +++++++++++-- damus/Views/NoteContentView.swift | 35 ++++++++++++++++--- 5 files changed, 86 insertions(+), 10 deletions(-) diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift index 20be57cb..c95cbe68 100644 --- a/damus/Components/ImageCarousel.swift +++ b/damus/Components/ImageCarousel.swift @@ -77,13 +77,14 @@ class CarouselModel: ObservableObject { // MARK: - Image Carousel @MainActor -struct ImageCarousel: View { +struct ImageCarousel: View { var urls: [MediaUrl] let evid: NoteId let state: DamusState @ObservedObject var model: CarouselModel + let content: ((_ dismiss: @escaping (() -> Void)) -> Content)? init(state: DamusState, evid: NoteId, urls: [MediaUrl]) { self.urls = urls @@ -91,6 +92,16 @@ struct ImageCarousel: View { self.state = state let media_model = state.events.get_cache_data(evid).media_metadata_model self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill)) + self.content = nil + } + + init(state: DamusState, evid: NoteId, urls: [MediaUrl], @ViewBuilder content: @escaping (_ dismiss: @escaping (() -> Void)) -> Content) { + self.urls = urls + self.evid = evid + self.state = state + let media_model = state.events.get_cache_data(evid).media_metadata_model + self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill)) + self.content = content } var filling: Bool { @@ -201,7 +212,16 @@ struct ImageCarousel: View { } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) .fullScreenCover(isPresented: $model.open_sheet) { - FullScreenCarouselView(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) + if let content { + FullScreenCarouselView(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) { + content({ // Dismiss closure + model.open_sheet = false + }) + } + } + else { + FullScreenCarouselView(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) + } } .frame(height: height) .onChange(of: model.selectedIndex) { value in @@ -297,7 +317,7 @@ struct ImageCarousel_Previews: PreviewProvider { static var previews: some View { let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!) let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!) - ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [test_video_url, url]) + ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [test_video_url, url]) .environmentObject(OrientationTracker()) } } diff --git a/damus/Components/TruncatedText.swift b/damus/Components/TruncatedText.swift index 62c909cb..8fca6895 100644 --- a/damus/Components/TruncatedText.swift +++ b/damus/Components/TruncatedText.swift @@ -9,7 +9,12 @@ import SwiftUI struct TruncatedText: View { let text: CompatibleText - let maxChars: Int = 280 + let maxChars: Int + + init(text: CompatibleText, maxChars: Int = 280) { + self.text = text + self.maxChars = maxChars + } var body: some View { let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars) diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift index 1570dcc0..c1bedb6d 100644 --- a/damus/Views/Events/TextEvent.swift +++ b/damus/Views/Events/TextEvent.swift @@ -19,8 +19,11 @@ struct EventViewOptions: OptionSet { static let nested = EventViewOptions(rawValue: 1 << 7) static let top_zap = EventViewOptions(rawValue: 1 << 8) static let no_mentions = EventViewOptions(rawValue: 1 << 9) + static let no_media = EventViewOptions(rawValue: 1 << 10) + static let truncate_content_very_short = EventViewOptions(rawValue: 1 << 11) static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] + static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short] } struct TextEvent: View { diff --git a/damus/Views/Images/FullScreenCarouselView.swift b/damus/Views/Images/FullScreenCarouselView.swift index 38290bf5..7e11ff57 100644 --- a/damus/Views/Images/FullScreenCarouselView.swift +++ b/damus/Views/Images/FullScreenCarouselView.swift @@ -7,7 +7,7 @@ import SwiftUI -struct FullScreenCarouselView: View { +struct FullScreenCarouselView: View { let video_controller: VideoController let urls: [MediaUrl] @@ -17,6 +17,25 @@ struct FullScreenCarouselView: View { let settings: UserSettingsStore @Binding var selectedIndex: Int + let content: (() -> Content)? + + init(video_controller: VideoController, urls: [MediaUrl], showMenu: Bool = true, settings: UserSettingsStore, selectedIndex: Binding, @ViewBuilder content: @escaping () -> Content) { + self.video_controller = video_controller + self.urls = urls + self.showMenu = showMenu + self.settings = settings + _selectedIndex = selectedIndex + self.content = content + } + + init(video_controller: VideoController, urls: [MediaUrl], showMenu: Bool = true, settings: UserSettingsStore, selectedIndex: Binding) { + self.video_controller = video_controller + self.urls = urls + self.showMenu = showMenu + self.settings = settings + _selectedIndex = selectedIndex + self.content = nil + } var tabViewIndicator: some View { HStack(spacing: 10) { @@ -99,6 +118,8 @@ struct FullScreenCarouselView: View { if (urls.count > 1) { tabViewIndicator } + + self.content?() } } .animation(.easeInOut, value: showMenu) @@ -115,7 +136,7 @@ fileprivate struct ImageViewPreview: View { let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!) var body: some View { - FullScreenCarouselView(video_controller: test_damus_state.video, urls: [test_video_url, url], settings: test_damus_state.settings, selectedIndex: $selectedIndex) + FullScreenCarouselView(video_controller: test_damus_state.video, urls: [test_video_url, url], settings: test_damus_state.settings, selectedIndex: $selectedIndex) .environmentObject(OrientationTracker()) } } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 1b664baf..491fe4ac 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -57,6 +57,10 @@ struct NoteContentView: View { return options.contains(.truncate_content) } + var truncate_very_short: Bool { + return options.contains(.truncate_content_very_short) + } + var with_padding: Bool { return options.contains(.wide) } @@ -73,7 +77,11 @@ struct NoteContentView: View { func truncatedText(content: CompatibleText) -> some View { Group { - if truncate { + if truncate_very_short { + TruncatedText(text: content, maxChars: 140) + .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) + } + else if truncate { TruncatedText(text: content) .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) } else { @@ -107,6 +115,19 @@ struct NoteContentView: View { } } + func fullscreen_preview(dismiss: @escaping () -> Void) -> some View { + VStack { + EventView(damus: damus_state, event: self.event, options: .embedded_text_only) + .padding(.top) + } + .background(.thinMaterial) + .preferredColorScheme(.dark) + .onTapGesture(perform: { + damus_state.nav.push(route: Route.Thread(thread: .init(event: self.event, damus_state: damus_state))) + dismiss() + }) + } + func MainContent(artifacts: NoteArtifactsSeparated) -> some View { VStack(alignment: .leading) { if size == .selected { @@ -135,13 +156,19 @@ struct NoteContentView: View { } if artifacts.media.count > 0 { - if !damus_state.settings.media_previews && !load_media { + if (self.options.contains(.no_media)) { + EmptyView() + } else if !damus_state.settings.media_previews && !load_media { loadMediaButton(artifacts: artifacts) } else if !blur_images || (!blur_images && !damus_state.settings.media_previews && load_media) { - ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media) + ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media) { dismiss in + fullscreen_preview(dismiss: dismiss) + } } else if blur_images || (blur_images && !damus_state.settings.media_previews && load_media) { ZStack { - ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media) + ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media) { dismiss in + fullscreen_preview(dismiss: dismiss) + } Blur() .onTapGesture { blur_images = false