Add event text to full-screen Carousel view

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 <daniel@daquino.me>
Link: 20240318222048.14226-4-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
Daniel D’Aquino
2024-03-18 22:21:28 +00:00
committed by William Casarin
parent 671b0b67ce
commit 250efd9755
5 changed files with 86 additions and 10 deletions

View File

@@ -77,13 +77,14 @@ class CarouselModel: ObservableObject {
// MARK: - Image Carousel // MARK: - Image Carousel
@MainActor @MainActor
struct ImageCarousel: View { struct ImageCarousel<Content: View>: View {
var urls: [MediaUrl] var urls: [MediaUrl]
let evid: NoteId let evid: NoteId
let state: DamusState let state: DamusState
@ObservedObject var model: CarouselModel @ObservedObject var model: CarouselModel
let content: ((_ dismiss: @escaping (() -> Void)) -> Content)?
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) { init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
self.urls = urls self.urls = urls
@@ -91,6 +92,16 @@ struct ImageCarousel: View {
self.state = state self.state = state
let media_model = state.events.get_cache_data(evid).media_metadata_model let media_model = state.events.get_cache_data(evid).media_metadata_model
self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill)) 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 { var filling: Bool {
@@ -201,7 +212,16 @@ struct ImageCarousel: View {
} }
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.fullScreenCover(isPresented: $model.open_sheet) { .fullScreenCover(isPresented: $model.open_sheet) {
FullScreenCarouselView(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) if let content {
FullScreenCarouselView<Content>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) {
content({ // Dismiss closure
model.open_sheet = false
})
}
}
else {
FullScreenCarouselView<AnyView>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex)
}
} }
.frame(height: height) .frame(height: height)
.onChange(of: model.selectedIndex) { value in .onChange(of: model.selectedIndex) { value in
@@ -297,7 +317,7 @@ struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!) 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")!) 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<AnyView>(state: test_damus_state, evid: test_note.id, urls: [test_video_url, url])
.environmentObject(OrientationTracker()) .environmentObject(OrientationTracker())
} }
} }

View File

@@ -9,7 +9,12 @@ import SwiftUI
struct TruncatedText: View { struct TruncatedText: View {
let text: CompatibleText 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 { var body: some View {
let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars) let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)

View File

@@ -19,8 +19,11 @@ struct EventViewOptions: OptionSet {
static let nested = EventViewOptions(rawValue: 1 << 7) static let nested = EventViewOptions(rawValue: 1 << 7)
static let top_zap = EventViewOptions(rawValue: 1 << 8) static let top_zap = EventViewOptions(rawValue: 1 << 8)
static let no_mentions = EventViewOptions(rawValue: 1 << 9) 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: 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 { struct TextEvent: View {

View File

@@ -7,7 +7,7 @@
import SwiftUI import SwiftUI
struct FullScreenCarouselView: View { struct FullScreenCarouselView<Content: View>: View {
let video_controller: VideoController let video_controller: VideoController
let urls: [MediaUrl] let urls: [MediaUrl]
@@ -17,6 +17,25 @@ struct FullScreenCarouselView: View {
let settings: UserSettingsStore let settings: UserSettingsStore
@Binding var selectedIndex: Int @Binding var selectedIndex: Int
let content: (() -> Content)?
init(video_controller: VideoController, urls: [MediaUrl], showMenu: Bool = true, settings: UserSettingsStore, selectedIndex: Binding<Int>, @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<Int>) {
self.video_controller = video_controller
self.urls = urls
self.showMenu = showMenu
self.settings = settings
_selectedIndex = selectedIndex
self.content = nil
}
var tabViewIndicator: some View { var tabViewIndicator: some View {
HStack(spacing: 10) { HStack(spacing: 10) {
@@ -99,6 +118,8 @@ struct FullScreenCarouselView: View {
if (urls.count > 1) { if (urls.count > 1) {
tabViewIndicator tabViewIndicator
} }
self.content?()
} }
} }
.animation(.easeInOut, value: showMenu) .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")!) let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!)
var body: some View { var body: some View {
FullScreenCarouselView(video_controller: test_damus_state.video, urls: [test_video_url, url], settings: test_damus_state.settings, selectedIndex: $selectedIndex) FullScreenCarouselView<AnyView>(video_controller: test_damus_state.video, urls: [test_video_url, url], settings: test_damus_state.settings, selectedIndex: $selectedIndex)
.environmentObject(OrientationTracker()) .environmentObject(OrientationTracker())
} }
} }

View File

@@ -57,6 +57,10 @@ struct NoteContentView: View {
return options.contains(.truncate_content) return options.contains(.truncate_content)
} }
var truncate_very_short: Bool {
return options.contains(.truncate_content_very_short)
}
var with_padding: Bool { var with_padding: Bool {
return options.contains(.wide) return options.contains(.wide)
} }
@@ -73,7 +77,11 @@ struct NoteContentView: View {
func truncatedText(content: CompatibleText) -> some View { func truncatedText(content: CompatibleText) -> some View {
Group { 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) TruncatedText(text: content)
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
} else { } 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 { func MainContent(artifacts: NoteArtifactsSeparated) -> some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if size == .selected { if size == .selected {
@@ -135,13 +156,19 @@ struct NoteContentView: View {
} }
if artifacts.media.count > 0 { 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) loadMediaButton(artifacts: artifacts)
} else if !blur_images || (!blur_images && !damus_state.settings.media_previews && load_media) { } 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) { } else if blur_images || (blur_images && !damus_state.settings.media_previews && load_media) {
ZStack { 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() Blur()
.onTapGesture { .onTapGesture {
blur_images = false blur_images = false