Display more details in player view

This commit is contained in:
Arkadiusz Fal
2021-08-22 21:13:33 +02:00
parent ea634390a6
commit f80b61f9c7
22 changed files with 716 additions and 36 deletions

View File

@@ -0,0 +1,55 @@
import Foundation
import SwiftUI
struct PlaybackBar: View {
@Environment(\.dismiss) private var dismiss
@ObservedObject var playbackState: PlaybackState
let video: Video
var body: some View {
HStack {
closeButton
.frame(minWidth: 0, maxWidth: 60, alignment: .leading)
Text(playbackFinishAtString)
.foregroundColor(.gray)
.font(.caption2)
.frame(minWidth: 0, maxWidth: .infinity)
Text(currentStreamString)
.foregroundColor(.gray)
.font(.caption2)
.frame(minWidth: 0, maxWidth: 60, alignment: .trailing)
}
.padding(4)
.background(.black)
}
var currentStreamString: String {
playbackState.stream != nil ? "\(playbackState.stream!.resolution.height)p" : ""
}
var playbackFinishAtString: String {
guard playbackState.time != nil else {
return "loading..."
}
let remainingSeconds = video.length - playbackState.time!.seconds
let timeFinishAt = Date.now.addingTimeInterval(remainingSeconds)
let timeFinishAtString = timeFinishAt.formatted(date: .omitted, time: .shortened)
return "finishes at \(timeFinishAtString)"
}
var closeButton: some View {
Button(action: { dismiss() }) {
Image(systemName: "chevron.down.circle.fill")
}
.accessibilityLabel(Text("Close"))
.buttonStyle(BorderlessButtonStyle())
.foregroundColor(.gray)
.keyboardShortcut(.cancelAction)
}
}

View File

@@ -1,10 +1,13 @@
import SwiftUI
struct Player: UIViewControllerRepresentable {
@ObservedObject var playbackState: PlaybackState
var video: Video?
func makeUIViewController(context _: Context) -> PlayerViewController {
let controller = PlayerViewController()
controller.playbackState = playbackState
controller.video = video
return controller

View File

@@ -7,7 +7,8 @@ final class PlayerViewController: UIViewController {
var playerLoaded = false
var player = AVPlayer()
var playerState: PlayerState! = PlayerState()
var playerState: PlayerState!
var playbackState: PlaybackState!
var playerViewController = AVPlayerViewController()
override func viewWillAppear(_ animated: Bool) {
@@ -33,6 +34,9 @@ final class PlayerViewController: UIViewController {
}
func loadPlayer() {
playerState = PlayerState()
playerState.playbackState = playbackState
guard !playerLoaded else {
return
}
@@ -45,7 +49,6 @@ final class PlayerViewController: UIViewController {
present(playerViewController, animated: false)
addItemDidPlayToEndTimeObserver()
#else
embedViewController()
#endif
@@ -111,6 +114,12 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
coordinator.animate(alongsideTransition: nil) { context in
if !context.isCancelled {
self.playerState.playingOutsideViewController = false
#if os(iOS)
if self.traitCollection.verticalSizeClass == .compact {
self.dismiss(animated: true)
}
#endif
}
}
}

View File

@@ -0,0 +1,132 @@
import Foundation
import SwiftUI
struct VideoDetails: View {
var video: Video
var body: some View {
VStack(alignment: .leading) {
Text(video.title)
.font(.title2.bold())
Text(video.author)
.foregroundColor(.secondary)
HStack(spacing: 4) {
if let published = video.publishedDate {
Text(published)
}
if let publishedAt = video.publishedAt {
if video.publishedDate != nil {
Text("")
.foregroundColor(.secondary)
.opacity(0.3)
}
Text(publishedAt.formatted(date: .abbreviated, time: .omitted))
}
}
.padding(.top, 4)
.font(.system(size: 12))
.foregroundColor(.secondary)
HStack {
if let views = video.viewsCount {
VideoDetail(title: "Views", detail: views)
}
if let likes = video.likesCount {
VideoDetail(title: "Likes", detail: likes, symbol: "hand.thumbsup.circle.fill", symbolColor: Color("VideoDetailLikesSymbolColor"))
}
if let dislikes = video.dislikesCount {
VideoDetail(title: "Dislikes", detail: dislikes, symbol: "hand.thumbsdown.circle.fill", symbolColor: Color("VideoDetailDislikesSymbolColor"))
}
}
.padding(.horizontal, 1)
.padding(.vertical, 4)
#if os(macOS)
ScrollView(.vertical) {
Text(video.description)
.font(.caption)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 100, alignment: .leading)
}
#else
Text(video.description)
.font(.caption)
#endif
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
HStack(alignment: .center, spacing: 0) {
Text("#")
.font(.system(size: 11).bold())
Text(keyword)
.frame(maxWidth: 500)
}.foregroundColor(.white)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color("VideoDetailLikesSymbolColor"))
.mask(RoundedRectangle(cornerRadius: 3))
.font(.caption)
}
}
.padding(.bottom, 10)
}
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding([.horizontal, .bottom])
}
var showScrollIndicators: Bool {
#if os(macOS)
false
#else
true
#endif
}
}
struct VideoDetail: View {
var title: String
var detail: String
var symbol = "eye.fill"
var symbolColor = Color.white
var body: some View {
VStack {
VStack(spacing: 0) {
HStack(alignment: .center, spacing: 4) {
Image(systemName: symbol)
.foregroundColor(symbolColor)
Text(title.uppercased())
Spacer()
}
.font(.caption2)
.padding([.leading, .top], 4)
.frame(alignment: .leading)
Divider()
.background(.gray)
.padding(.vertical, 4)
Text(detail)
.shadow(radius: 1.0)
.font(.title3.bold())
}
}
.foregroundColor(.white)
.background(Color("VideoDetailBackgroundColor"))
.cornerRadius(6)
.overlay(RoundedRectangle(cornerRadius: 6)
.stroke(Color("VideoDetailBorderColor"), lineWidth: 1))
.frame(maxWidth: 90)
}
}

View File

@@ -0,0 +1,42 @@
import Foundation
import SwiftUI
struct VideoDetailsPaddingModifier: ViewModifier {
let geometry: GeometryProxy
let aspectRatio: CGFloat?
let minimumHeightLeft: CGFloat
let additionalPadding: CGFloat
init(
geometry: GeometryProxy,
aspectRatio: CGFloat? = nil,
minimumHeightLeft: CGFloat? = nil,
additionalPadding: CGFloat = 35.00
) {
self.geometry = geometry
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
self.additionalPadding = additionalPadding
}
var usedAspectRatio: CGFloat {
guard aspectRatio != nil else {
return VideoPlayerView.defaultAspectRatio
}
return [aspectRatio!, VideoPlayerView.defaultAspectRatio].min()!
}
var playerHeight: CGFloat {
[geometry.size.width / usedAspectRatio, geometry.size.height - minimumHeightLeft].min()!
}
var topPadding: CGFloat {
playerHeight + additionalPadding
}
func body(content: Content) -> some View {
content
.padding(.top, topPadding)
}
}

View File

@@ -0,0 +1,70 @@
import Foundation
import SwiftUI
struct VideoPlayerSizeModifier: ViewModifier {
let geometry: GeometryProxy
let aspectRatio: CGFloat?
let minimumHeightLeft: CGFloat
#if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass
#endif
init(
geometry: GeometryProxy,
aspectRatio: CGFloat? = nil,
minimumHeightLeft: CGFloat? = nil
) {
self.geometry = geometry
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
}
func body(content: Content) -> some View {
content
.frame(maxHeight: maxHeight)
.aspectRatio(usedAspectRatio, contentMode: usedAspectRatioContentMode)
.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
}
var usedAspectRatio: CGFloat {
guard aspectRatio != nil else {
return VideoPlayerView.defaultAspectRatio
}
let ratio = [aspectRatio!, VideoPlayerView.defaultAspectRatio].min()!
let viewRatio = geometry.size.width / geometry.size.height
#if os(iOS)
return verticalSizeClass == .regular ? ratio : viewRatio
#else
return ratio
#endif
}
var usedAspectRatioContentMode: ContentMode {
#if os(iOS)
verticalSizeClass == .regular ? .fit : .fill
#else
.fit
#endif
}
var maxHeight: CGFloat {
#if os(iOS)
verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity
#else
geometry.size.height - minimumHeightLeft
#endif
}
var edgesIgnoringSafeArea: Edge.Set {
let empty = Edge.Set()
#if os(iOS)
return verticalSizeClass == .compact ? .all : empty
#else
return empty
#endif
}
}

View File

@@ -3,11 +3,24 @@ import Siesta
import SwiftUI
struct VideoPlayerView: View {
static let defaultAspectRatio: CGFloat = 1.77777778
static var defaultMinimumHeightLeft: CGFloat {
#if os(macOS)
300
#else
200
#endif
}
@EnvironmentObject<NavigationState> private var navigationState
@ObservedObject private var store = Store<Video>()
@Environment(\.dismiss) private var dismiss
@ObservedObject private var playbackState = PlaybackState()
#if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass
#endif
var resource: Resource {
InvidiousAPI.shared.video(video.id)
@@ -21,25 +34,50 @@ struct VideoPlayerView: View {
}
var body: some View {
VStack {
Player(video: video)
.frame(alignment: .leading)
VStack(spacing: 0) {
#if os(tvOS)
Player(playbackState: playbackState, video: video)
#else
GeometryReader { geometry in
VStack(spacing: 0) {
#if os(iOS)
if verticalSizeClass == .regular {
PlaybackBar(playbackState: playbackState, video: video)
}
#elseif os(macOS)
PlaybackBar(playbackState: playbackState, video: video)
#endif
#if !os(tvOS)
ScrollView(.vertical) {
VStack(alignment: .leading) {
Text(video.title)
Text(video.author)
Button("Done") {
dismiss()
}
.keyboardShortcut(.cancelAction)
Player(playbackState: playbackState, video: video)
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: playbackState.aspectRatio))
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(.black)
VStack(spacing: 0) {
#if os(iOS)
if verticalSizeClass == .regular {
ScrollView(.vertical, showsIndicators: showScrollIndicators) {
if let video = store.item {
VideoDetails(video: video)
} else {
VideoDetails(video: video)
}
}
}
#else
if let video = store.item {
VideoDetails(video: video)
} else {
VideoDetails(video: video)
}
#endif
}
.modifier(VideoDetailsPaddingModifier(geometry: geometry, aspectRatio: playbackState.aspectRatio))
}
.animation(.linear(duration: 0.2), value: playbackState.aspectRatio)
#endif
}
.onAppear {
resource.loadIfNeeded()
}
@@ -51,8 +89,29 @@ struct VideoPlayerView: View {
}
#if os(macOS)
.navigationTitle(video.title)
.frame(maxWidth: 1000, minHeight: 700)
#elseif os(iOS)
.navigationBarTitle(video.title, displayMode: .inline)
#endif
}
var showScrollIndicators: Bool {
#if os(macOS)
false
#else
true
#endif
}
}
struct VideoPlayerView_Previews: PreviewProvider {
static var previews: some View {
VStack {
Spacer()
}
.sheet(isPresented: .constant(true)) {
VideoPlayerView(Video.fixture)
.environmentObject(NavigationState())
}
}
}