commit 247f313b54296b3bb8ff02912279cc8b623c7fde
parent b31b917b70dfd045996f5b6b30ae0b38d49db981
Author: William Casarin <jb55@jb55.com>
Date: Wed, 20 Mar 2024 09:57:10 +0000
Merge branch 'video-controls'
Diffstat:
14 files changed, 423 insertions(+), 163 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -375,7 +375,7 @@
4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD502E2A2DA45800A229DB /* MediaView.swift */; };
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */; };
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
- 4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
+ 4CFF8F6729CC9E3A008DB934 /* FullScreenCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* FullScreenCarouselView.swift */; };
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; };
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; };
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
@@ -386,7 +386,7 @@
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A62A34915F006AE6DC /* RelayModel.swift */; };
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A82A3495B6006AE6DC /* RelayModelCache.swift */; };
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */; };
- 50A16FFB2AA6C06600DFEC1F /* AVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFA2AA6C06600DFEC1F /* AVPlayerView.swift */; };
+ 50A16FFB2AA6C06600DFEC1F /* DamusAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFA2AA6C06600DFEC1F /* DamusAVPlayerView.swift */; };
50A16FFD2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFC2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift */; };
50A16FFF2AA76A0900DFEC1F /* VideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFE2AA76A0900DFEC1F /* VideoController.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
@@ -431,8 +431,6 @@
B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; };
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */; };
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
- BA0F0A6F2B36207E001641B2 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */; };
- BA10192F2B449556009C57DA /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA10192E2B449556009C57DA /* CameraPreview.swift */; };
B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; };
B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */; };
@@ -454,6 +452,7 @@
D7100C5A2B76FD5100C59298 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C592B76FD5100C59298 /* LogoView.swift */; };
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5B2B77016700C59298 /* IAPProductStateView.swift */; };
D7100C5E2B7709ED00C59298 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
+ D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71AC4CB2BA8E3480076268E /* VisibilityTracker.swift */; };
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; };
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
D723411A2B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
@@ -1296,7 +1295,7 @@
4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleView.swift; sourceTree = "<group>"; };
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
- 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
+ 4CFF8F6629CC9E3A008DB934 /* FullScreenCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCarouselView.swift; sourceTree = "<group>"; };
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; };
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; };
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
@@ -1307,7 +1306,7 @@
504323A62A34915F006AE6DC /* RelayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayModel.swift; sourceTree = "<group>"; };
504323A82A3495B6006AE6DC /* RelayModelCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayModelCache.swift; sourceTree = "<group>"; };
5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = "<group>"; };
- 50A16FFA2AA6C06600DFEC1F /* AVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerView.swift; sourceTree = "<group>"; };
+ 50A16FFA2AA6C06600DFEC1F /* DamusAVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAVPlayerView.swift; sourceTree = "<group>"; };
50A16FFC2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoPlayerViewModel.swift; sourceTree = "<group>"; };
50A16FFE2AA76A0900DFEC1F /* VideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoController.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
@@ -1351,8 +1350,6 @@
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItemTests.swift; sourceTree = "<group>"; usesTabs = 0; };
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
- BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = "<group>"; };
- BA10192E2B449556009C57DA /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItem.swift; sourceTree = "<group>"; usesTabs = 0; };
B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusDuration.swift; sourceTree = "<group>"; usesTabs = 0; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
@@ -1373,6 +1370,7 @@
D7100C592B76FD5100C59298 /* LogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoView.swift; sourceTree = "<group>"; };
D7100C5B2B77016700C59298 /* IAPProductStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPProductStateView.swift; sourceTree = "<group>"; };
D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleStoreKitManager.swift; sourceTree = "<group>"; };
+ D71AC4CB2BA8E3480076268E /* VisibilityTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibilityTracker.swift; sourceTree = "<group>"; };
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; };
D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleEnvironment.swift; sourceTree = "<group>"; };
D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; };
@@ -1728,7 +1726,7 @@
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */,
50A16FFC2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift */,
50A16FFE2AA76A0900DFEC1F /* VideoController.swift */,
- 50A16FFA2AA6C06600DFEC1F /* AVPlayerView.swift */,
+ 50A16FFA2AA6C06600DFEC1F /* DamusAVPlayerView.swift */,
);
path = Video;
sourceTree = "<group>";
@@ -1977,6 +1975,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
+ D71AC4CA2BA8E3320076268E /* Extensions */,
BA3759952ABCCF360018D73B /* Camera */,
F71694E82A66221E001F4053 /* Onboarding */,
4C190F232A547D1700027FD5 /* NostrScript */,
@@ -2653,7 +2652,7 @@
isa = PBXGroup;
children = (
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */,
- 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */,
+ 4CFF8F6629CC9E3A008DB934 /* FullScreenCarouselView.swift */,
6439E013296790CF0020672B /* ProfilePicImageView.swift */,
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */,
4CFD502E2A2DA45800A229DB /* MediaView.swift */,
@@ -2704,6 +2703,14 @@
path = Detail;
sourceTree = "<group>";
};
+ D71AC4CA2BA8E3320076268E /* Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ D71AC4CB2BA8E3480076268E /* VisibilityTracker.swift */,
+ );
+ path = Extensions;
+ sourceTree = "<group>";
+ };
D72A2D032AD9C165002AFF62 /* Mocking */ = {
isa = PBXGroup;
children = (
@@ -3171,7 +3178,7 @@
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
D7ADD3E02B538D4200F104C4 /* DamusPurpleURLSheetView.swift in Sources */,
- 4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
+ 4CFF8F6729CC9E3A008DB934 /* FullScreenCarouselView.swift in Sources */,
4CA927632A290EB10098A105 /* EventTop.swift in Sources */,
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */,
@@ -3203,6 +3210,7 @@
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
+ D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */,
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
@@ -3409,7 +3417,7 @@
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
- 50A16FFB2AA6C06600DFEC1F /* AVPlayerView.swift in Sources */,
+ 50A16FFB2AA6C06600DFEC1F /* DamusAVPlayerView.swift in Sources */,
4CA352A82A76B37E003BB08B /* NewMutesNotify.swift in Sources */,
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
7527271E2A93FF0100214108 /* Block.swift in Sources */,
diff --git 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<Content: View>: 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 {
@@ -132,7 +143,7 @@ struct ImageCarousel: View {
model.open_sheet = true
}
case .video(let url):
- DamusVideoPlayer(url: url, video_size: $model.video_size, controller: state.video)
+ DamusVideoPlayer(url: url, video_size: $model.video_size, controller: state.video, style: .preview(on_tap: { model.open_sheet = true }))
.onChange(of: model.video_size) { size in
guard let size else { return }
@@ -201,7 +212,16 @@ struct ImageCarousel: View {
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.fullScreenCover(isPresented: $model.open_sheet) {
- ImageView(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)
.onChange(of: model.selectedIndex) { value in
@@ -296,7 +316,9 @@ public struct ImageFill {
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
- ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [url, url])
+ let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!)
+ ImageCarousel<AnyView>(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
@@ -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
@@ -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/Extensions/VisibilityTracker.swift b/damus/Views/Extensions/VisibilityTracker.swift
@@ -0,0 +1,36 @@
+//
+// VisibilityTracker.swift
+// damus
+//
+// Created by Daniel D’Aquino on 2024-03-18.
+//
+// Based on code examples shown in this article: https://medium.com/@jackvanderpump/how-to-detect-is-an-element-is-visible-in-swiftui-9ff58ca72339
+
+import Foundation
+import SwiftUI
+
+extension View {
+ func on_visibility_change(perform visibility_change_notifier: @escaping (Bool) -> Void, edge: Alignment = .center) -> some View {
+ self.modifier(VisibilityTracker(visibility_change_notifier: visibility_change_notifier, edge: edge))
+ }
+}
+
+struct VisibilityTracker: ViewModifier {
+ let visibility_change_notifier: (Bool) -> Void
+ let edge: Alignment
+
+ func body(content: Content) -> some View {
+ content
+ .overlay(
+ LazyVStack {
+ Color.clear
+ .onAppear {
+ visibility_change_notifier(true)
+ }
+ .onDisappear {
+ visibility_change_notifier(false)
+ }
+ },
+ alignment: edge)
+ }
+}
diff --git a/damus/Views/Images/FullScreenCarouselView.swift b/damus/Views/Images/FullScreenCarouselView.swift
@@ -0,0 +1,171 @@
+//
+// FullScreenCarouselView.swift
+// damus
+//
+// Created by William Casarin on 2023-03-23.
+//
+
+import SwiftUI
+
+struct FullScreenCarouselView<Content: View>: View {
+ let video_controller: VideoController
+ let urls: [MediaUrl]
+
+ @Environment(\.presentationMode) var presentationMode
+
+ @State var showMenu = true
+
+ let settings: UserSettingsStore
+ @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 = State(initialValue: 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 = State(initialValue: showMenu)
+ self.settings = settings
+ _selectedIndex = selectedIndex
+ self.content = nil
+ }
+
+ var tabViewIndicator: some View {
+ HStack(spacing: 10) {
+ ForEach(urls.indices, id: \.self) { index in
+ Capsule()
+ .fill(index == selectedIndex ? Color.white : Color.damusMediumGrey)
+ .frame(width: 7, height: 7)
+ .onTapGesture {
+ selectedIndex = index
+ }
+ }
+ }
+ .padding()
+ .clipShape(Capsule())
+ }
+
+ var background: some ShapeStyle {
+ if case .video = urls[safe: selectedIndex] {
+ return AnyShapeStyle(Color.black)
+ }
+ else {
+ return AnyShapeStyle(.regularMaterial)
+ }
+ }
+
+ var background_color: UIColor {
+ return .black
+ }
+
+ var body: some View {
+ ZStack {
+ Color(self.background_color)
+ .ignoresSafeArea()
+
+ TabView(selection: $selectedIndex) {
+ ForEach(urls.indices, id: \.self) { index in
+ VStack {
+ if case .video = urls[safe: index] {
+ ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings)
+ .clipped() // SwiftUI hack from https://stackoverflow.com/a/74401288 to make playback controls show up within the TabView
+ .aspectRatio(contentMode: .fit)
+ .padding(.top, Theme.safeAreaInsets?.top)
+ .padding(.bottom, Theme.safeAreaInsets?.bottom)
+ .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
+ presentationMode.wrappedValue.dismiss()
+ }))
+ .ignoresSafeArea()
+ }
+ else {
+ ZoomableScrollView {
+ ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings)
+ .aspectRatio(contentMode: .fit)
+ .padding(.top, Theme.safeAreaInsets?.top)
+ .padding(.bottom, Theme.safeAreaInsets?.bottom)
+ }
+ .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
+ presentationMode.wrappedValue.dismiss()
+ }))
+ .ignoresSafeArea()
+ }
+ }.tag(index)
+ }
+ }
+ .ignoresSafeArea()
+ .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
+ .gesture(TapGesture(count: 2).onEnded {
+ // Prevents menu from hiding on double tap
+ })
+ .gesture(TapGesture(count: 1).onEnded {
+ showMenu.toggle()
+ })
+ .overlay(
+ GeometryReader { geo in
+ VStack {
+ if showMenu {
+ NavDismissBarView(showBackgroundCircle: false)
+ .foregroundColor(.white)
+ Spacer()
+
+ if (urls.count > 1) {
+ tabViewIndicator
+ }
+
+ self.content?()
+ }
+ }
+ .animation(.easeInOut, value: showMenu)
+ .padding(.bottom, geo.safeAreaInsets.bottom == 0 ? 12 : 0)
+ }
+ )
+ }
+ }
+}
+
+fileprivate struct FullScreenCarouselPreviewView<Content: View>: View {
+ @State var selectedIndex: Int = 0
+ 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 custom_content: (() -> Content)?
+
+ init(content: (() -> Content)? = nil) {
+ self.custom_content = content
+ }
+
+ var body: some View {
+ FullScreenCarouselView(video_controller: test_damus_state.video, urls: [test_video_url, url], settings: test_damus_state.settings, selectedIndex: $selectedIndex) {
+ self.custom_content?()
+ }
+ .environmentObject(OrientationTracker())
+ }
+}
+
+struct FullScreenCarouselView_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ FullScreenCarouselPreviewView<AnyView>()
+ .previewDisplayName("No custom content on overlay")
+
+ FullScreenCarouselPreviewView(content: {
+ HStack {
+ Spacer()
+
+ Text("Some content")
+ .padding()
+ .foregroundColor(.white)
+
+ Spacer()
+ }.background(.ultraThinMaterial)
+ })
+ .previewDisplayName("Custom content on overlay")
+ }
+ }
+}
diff --git a/damus/Views/Images/ImageContainerView.swift b/damus/Views/Images/ImageContainerView.swift
@@ -43,19 +43,26 @@ struct ImageContainerView: View {
var body: some View {
Group {
switch url {
- case .image(let url):
- Img(url: url)
- case .video(let url):
- DamusVideoPlayer(url: url, video_size: .constant(nil), controller: video_controller)
+ case .image(let url):
+ Img(url: url)
+ case .video(let url):
+ DamusVideoPlayer(url: url, video_size: .constant(nil), controller: video_controller, style: .full, visibility_tracking_method: .generic)
}
}
}
}
let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
+fileprivate let test_video_url = URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!
struct ImageContainerView_Previews: PreviewProvider {
static var previews: some View {
- ImageContainerView(video_controller: test_damus_state.video, url: .image(test_image_url), settings: test_damus_state.settings)
+ Group {
+ ImageContainerView(video_controller: test_damus_state.video, url: .image(test_image_url), settings: test_damus_state.settings)
+ .previewDisplayName("Image")
+ ImageContainerView(video_controller: test_damus_state.video, url: .video(test_video_url), settings: test_damus_state.settings)
+ .previewDisplayName("Video")
+ }
+ .environmentObject(OrientationTracker())
}
}
diff --git a/damus/Views/Images/ImageView.swift b/damus/Views/Images/ImageView.swift
@@ -1,90 +0,0 @@
-//
-// ImageView.swift
-// damus
-//
-// Created by William Casarin on 2023-03-23.
-//
-
-import SwiftUI
-
-struct ImageView: View {
- let video_controller: VideoController
- let urls: [MediaUrl]
-
- @Environment(\.presentationMode) var presentationMode
-
- @State var showMenu = true
-
- let settings: UserSettingsStore
- @Binding var selectedIndex: Int
-
- var tabViewIndicator: some View {
- HStack(spacing: 10) {
- ForEach(urls.indices, id: \.self) { index in
- Capsule()
- .fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
- .frame(width: 7, height: 7)
- .onTapGesture {
- selectedIndex = index
- }
- }
- }
- .padding()
- .background(.regularMaterial)
- .clipShape(Capsule())
- }
-
- var body: some View {
- ZStack {
- Color(.systemBackground)
- .ignoresSafeArea()
-
- TabView(selection: $selectedIndex) {
- ForEach(urls.indices, id: \.self) { index in
- ZoomableScrollView {
- ImageContainerView(video_controller: video_controller, url: urls[index], settings: settings)
- .aspectRatio(contentMode: .fit)
- .padding(.top, Theme.safeAreaInsets?.top)
- .padding(.bottom, Theme.safeAreaInsets?.bottom)
- }
- .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
- presentationMode.wrappedValue.dismiss()
- }))
- .ignoresSafeArea()
- .tag(index)
- }
- }
- .ignoresSafeArea()
- .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
- .gesture(TapGesture(count: 2).onEnded {
- // Prevents menu from hiding on double tap
- })
- .gesture(TapGesture(count: 1).onEnded {
- showMenu.toggle()
- })
- .overlay(
- GeometryReader { geo in
- VStack {
- if showMenu {
- NavDismissBarView()
- Spacer()
-
- if (urls.count > 1) {
- tabViewIndicator
- }
- }
- }
- .animation(.easeInOut, value: showMenu)
- .padding(.bottom, geo.safeAreaInsets.bottom == 0 ? 12 : 0)
- }
- )
- }
- }
-}
-
-struct ImageView_Previews: PreviewProvider {
- static var previews: some View {
- let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
- ImageView(video_controller: test_damus_state.video, urls: [url], settings: test_damus_state.settings, selectedIndex: Binding.constant(0))
- }
-}
diff --git a/damus/Views/Images/ProfilePicImageView.swift b/damus/Views/Images/ProfilePicImageView.swift
@@ -42,16 +42,27 @@ struct ProfileImageContainerView: View {
struct NavDismissBarView: View {
@Environment(\.presentationMode) var presentationMode
+ let showBackgroundCircle: Bool
+
+ init(showBackgroundCircle: Bool = true) {
+ self.showBackgroundCircle = showBackgroundCircle
+ }
var body: some View {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
- Image("close")
- .frame(width: 33, height: 33)
- .background(.regularMaterial)
- .clipShape(Circle())
+ if showBackgroundCircle {
+ Image("close")
+ .frame(width: 33, height: 33)
+ .background(.regularMaterial)
+ .clipShape(Circle())
+ }
+ else {
+ Image("close")
+ .frame(width: 33, height: 33)
+ }
})
Spacer()
diff --git 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
diff --git a/damus/Views/Video/AVPlayerView.swift b/damus/Views/Video/AVPlayerView.swift
@@ -1,31 +0,0 @@
-//
-// AVPlayerView.swift
-// damus
-//
-// Created by Bryan Montz on 9/4/23.
-//
-
-import Foundation
-import AVKit
-import SwiftUI
-
-struct AVPlayerView: UIViewControllerRepresentable {
-
- let player: AVPlayer
-
- func makeUIViewController(context: Context) -> AVPlayerViewController {
- AVPlayerViewController()
- }
-
- func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
- if uiViewController.player == nil {
- uiViewController.player = player
- player.play()
- }
- }
-
- static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: ()) {
- uiViewController.player?.pause()
- uiViewController.player = nil
- }
-}
diff --git a/damus/Views/Video/DamusAVPlayerView.swift b/damus/Views/Video/DamusAVPlayerView.swift
@@ -0,0 +1,34 @@
+//
+// AVPlayerView.swift
+// damus
+//
+// Created by Bryan Montz on 9/4/23.
+//
+
+import Foundation
+import AVKit
+import SwiftUI
+
+struct DamusAVPlayerView: UIViewControllerRepresentable {
+
+ let player: AVPlayer
+ var controller: AVPlayerViewController
+ let show_playback_controls: Bool
+
+ func makeUIViewController(context: Context) -> AVPlayerViewController {
+ self.controller.showsPlaybackControls = show_playback_controls
+ return self.controller
+ }
+
+ func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
+ if uiViewController.player == nil {
+ uiViewController.player = player
+ player.play()
+ }
+ }
+
+ static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: ()) {
+ uiViewController.player?.pause()
+ uiViewController.player = nil
+ }
+}
diff --git a/damus/Views/Video/DamusVideoPlayer.swift b/damus/Views/Video/DamusVideoPlayer.swift
@@ -20,10 +20,22 @@ struct DamusVideoPlayer: View {
let url: URL
@StateObject var model: DamusVideoPlayerViewModel
@EnvironmentObject private var orientationTracker: OrientationTracker
+ let style: Style
+ let visibility_tracking_method: VisibilityTrackingMethod
+ @State var isVisible: Bool = false
- init(url: URL, video_size: Binding<CGSize?>, controller: VideoController) {
+ init(url: URL, video_size: Binding<CGSize?>, controller: VideoController, style: Style, visibility_tracking_method: VisibilityTrackingMethod = .y_scroll) {
self.url = url
- _model = StateObject(wrappedValue: DamusVideoPlayerViewModel(url: url, video_size: video_size, controller: controller))
+ let mute: Bool?
+ if case .full = style {
+ mute = false
+ }
+ else {
+ mute = nil
+ }
+ _model = StateObject(wrappedValue: DamusVideoPlayerViewModel(url: url, video_size: video_size, controller: controller, mute: mute))
+ self.visibility_tracking_method = visibility_tracking_method
+ self.style = style
}
var body: some View {
@@ -31,7 +43,15 @@ struct DamusVideoPlayer: View {
let localFrame = geo.frame(in: .local)
let centerY = globalCoordinate(localX: 0, localY: localFrame.midY, localGeometry: geo).y
ZStack {
- AVPlayerView(player: model.player)
+ if case .full = self.style {
+ DamusAVPlayerView(player: model.player, controller: model.player_view_controller, show_playback_controls: true)
+ }
+ if case .preview(let on_tap) = self.style {
+ DamusAVPlayerView(player: model.player, controller: model.player_view_controller, show_playback_controls: false)
+ .simultaneousGesture(TapGesture().onEnded({
+ on_tap?()
+ }))
+ }
if model.is_loading {
ProgressView()
@@ -40,22 +60,35 @@ struct DamusVideoPlayer: View {
.scaleEffect(CGSize(width: 1.5, height: 1.5))
}
- if model.has_audio {
- mute_button
+ if case .preview = self.style {
+ if model.has_audio {
+ mute_button
+ }
}
if model.is_live {
live_indicator
}
}
.onChange(of: centerY) { _ in
- update_is_visible(centerY: centerY)
+ if case .y_scroll = visibility_tracking_method {
+ update_is_visible(centerY: centerY)
+ }
}
+ .on_visibility_change(perform: { new_visibility in
+ if case .generic = visibility_tracking_method {
+ model.set_view_is_visible(new_visibility)
+ }
+ })
.onAppear {
- update_is_visible(centerY: centerY)
+ if case .y_scroll = visibility_tracking_method {
+ update_is_visible(centerY: centerY)
+ }
}
}
.onDisappear {
- model.view_did_disappear()
+ if case .y_scroll = visibility_tracking_method {
+ model.view_did_disappear()
+ }
}
}
@@ -115,9 +148,31 @@ struct DamusVideoPlayer: View {
Spacer()
}
}
+
+ enum Style {
+ /// A full video player with playback controls
+ case full
+ /// A style suitable for muted, auto-playing videos on a feed
+ case preview(on_tap: (() -> Void)?)
+ }
+
+ enum VisibilityTrackingMethod {
+ /// Detects visibility based on its Y position relative to viewport. Ideal for long feeds
+ case y_scroll
+ /// Detects visibility based whether the view intersects with the viewport
+ case generic
+ }
}
struct DamusVideoPlayer_Previews: PreviewProvider {
static var previews: some View {
- DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController())
+ Group {
+ DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController(), style: .full)
+ .environmentObject(OrientationTracker())
+ .previewDisplayName("Full video player")
+
+ DamusVideoPlayer(url: URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!, video_size: .constant(nil), controller: VideoController(), style: .preview(on_tap: nil))
+ .environmentObject(OrientationTracker())
+ .previewDisplayName("Preview video player")
+ }
}
}
diff --git a/damus/Views/Video/DamusVideoPlayerViewModel.swift b/damus/Views/Video/DamusVideoPlayerViewModel.swift
@@ -6,6 +6,7 @@
//
import AVFoundation
+import AVKit
import Combine
import Foundation
import SwiftUI
@@ -27,7 +28,8 @@ final class DamusVideoPlayerViewModel: ObservableObject {
private let url: URL
private let player_item: AVPlayerItem
let player: AVPlayer
- private let controller: VideoController
+ fileprivate let controller: VideoController
+ let player_view_controller = AVPlayerViewController()
let id = UUID()
@Published var has_audio = false
@@ -55,7 +57,7 @@ final class DamusVideoPlayerViewModel: ObservableObject {
}
}
- init(url: URL, video_size: Binding<CGSize?>, controller: VideoController) {
+ init(url: URL, video_size: Binding<CGSize?>, controller: VideoController, mute: Bool? = nil) {
self.url = url
player_item = AVPlayerItem(url: url)
player = AVPlayer(playerItem: player_item)
@@ -66,7 +68,7 @@ final class DamusVideoPlayerViewModel: ObservableObject {
await load()
}
- is_muted = controller.should_mute_video(url: url)
+ is_muted = mute ?? controller.should_mute_video(url: url)
player.isMuted = is_muted
NotificationCenter.default.addObserver(