Merge branch 'video-controls'
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
36
damus/Views/Extensions/VisibilityTracker.swift
Normal file
36
damus/Views/Extensions/VisibilityTracker.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
171
damus/Views/Images/FullScreenCarouselView.swift
Normal file
171
damus/Views/Images/FullScreenCarouselView.swift
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,12 +9,15 @@ import Foundation
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
|
||||
struct AVPlayerView: UIViewControllerRepresentable {
|
||||
struct DamusAVPlayerView: UIViewControllerRepresentable {
|
||||
|
||||
let player: AVPlayer
|
||||
var controller: AVPlayerViewController
|
||||
let show_playback_controls: Bool
|
||||
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
AVPlayerViewController()
|
||||
self.controller.showsPlaybackControls = show_playback_controls
|
||||
return self.controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user