mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Display more details in player view
This commit is contained in:
parent
ea634390a6
commit
f80b61f9c7
@ -6,17 +6,21 @@ extension Video {
|
||||
|
||||
return Video(
|
||||
id: UUID().uuidString,
|
||||
title: "Relaxing Piano Music",
|
||||
title: "Relaxing Piano Music that will make you feel amazingly good",
|
||||
author: "Fancy Videotuber",
|
||||
length: 582,
|
||||
published: "7 years ago",
|
||||
views: 1024,
|
||||
views: 21534,
|
||||
channelID: "AbCdEFgHI",
|
||||
description: "Some relaxing live piano music",
|
||||
genre: "Music",
|
||||
thumbnails: Thumbnail.fixturesForAllQualities(videoId: id),
|
||||
live: false,
|
||||
upcoming: false
|
||||
upcoming: false,
|
||||
publishedAt: Date.now,
|
||||
likes: 37333,
|
||||
dislikes: 30,
|
||||
keywords: ["very", "cool", "video", "msfs 2020", "757", "747", "A380", "737-900", "MOD", "Zibo", "MD80", "MD11", "Rotate", "Laminar", "787", "A350", "MSFS", "MS2020", "Microsoft Flight Simulator", "Microsoft", "Flight", "Simulator", "SIM", "World", "Ortho", "Flying", "Boeing MAX"]
|
||||
)
|
||||
}
|
||||
|
||||
|
25
Model/PlaybackState.swift
Normal file
25
Model/PlaybackState.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import CoreMedia
|
||||
import Foundation
|
||||
|
||||
final class PlaybackState: ObservableObject {
|
||||
@Published var stream: Stream?
|
||||
@Published var time: CMTime?
|
||||
|
||||
var aspectRatio: CGFloat? {
|
||||
let tracks = stream?.videoAsset.tracks(withMediaType: .video)
|
||||
|
||||
guard tracks != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let size: CGSize! = tracks!.first.flatMap {
|
||||
tracks!.isEmpty ? nil : $0.naturalSize.applying($0.preferredTransform)
|
||||
}
|
||||
|
||||
guard size != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return size.width / size.height
|
||||
}
|
||||
}
|
@ -14,19 +14,21 @@ final class PlayerState: ObservableObject {
|
||||
|
||||
private var compositions = [Stream: AVMutableComposition]()
|
||||
|
||||
private(set) var currentTime: CMTime?
|
||||
private(set) var savedTime: CMTime?
|
||||
|
||||
private(set) var currentRate: Float = 0.0
|
||||
static let availableRates: [Double] = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
|
||||
|
||||
let maxResolution: Stream.Resolution?
|
||||
var playbackState: PlaybackState?
|
||||
var timeObserver: Any?
|
||||
|
||||
let maxResolution: Stream.Resolution?
|
||||
|
||||
var playingOutsideViewController = false
|
||||
|
||||
init(_ video: Video? = nil, maxResolution: Stream.Resolution? = nil) {
|
||||
init(_ video: Video? = nil, playbackState: PlaybackState? = nil, maxResolution: Stream.Resolution? = nil) {
|
||||
self.video = video
|
||||
self.playbackState = playbackState
|
||||
self.maxResolution = maxResolution
|
||||
}
|
||||
|
||||
@ -101,6 +103,10 @@ final class PlayerState: ObservableObject {
|
||||
DispatchQueue.main.async {
|
||||
self.saveTime()
|
||||
self.player?.replaceCurrentItem(with: self.playerItemWithMetadata(for: stream))
|
||||
self.playbackState?.stream = stream
|
||||
if self.timeObserver == nil {
|
||||
self.addTimeObserver()
|
||||
}
|
||||
self.player?.playImmediately(atRate: 1.0)
|
||||
self.seekToSavedTime()
|
||||
}
|
||||
@ -245,9 +251,15 @@ final class PlayerState: ObservableObject {
|
||||
let interval = CMTime(value: 1, timescale: 1)
|
||||
|
||||
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
|
||||
guard self.player != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.player.rate != self.currentRate, self.player.rate != 0, self.currentRate != 0 {
|
||||
self.player.rate = self.currentRate
|
||||
}
|
||||
|
||||
self.playbackState?.time = self.player.currentTime()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,11 @@ struct Video: Identifiable, Equatable {
|
||||
var streams = [Stream]()
|
||||
var hlsUrl: URL?
|
||||
|
||||
var publishedAt: Date?
|
||||
var likes: Int?
|
||||
var dislikes: Int?
|
||||
var keywords = [String]()
|
||||
|
||||
init(
|
||||
id: String,
|
||||
title: String,
|
||||
@ -37,7 +42,11 @@ struct Video: Identifiable, Equatable {
|
||||
thumbnails: [Thumbnail] = [],
|
||||
indexID: String? = nil,
|
||||
live: Bool = false,
|
||||
upcoming: Bool = false
|
||||
upcoming: Bool = false,
|
||||
publishedAt: Date? = nil,
|
||||
likes: Int? = nil,
|
||||
dislikes: Int? = nil,
|
||||
keywords: [String] = []
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
@ -52,6 +61,10 @@ struct Video: Identifiable, Equatable {
|
||||
self.indexID = indexID
|
||||
self.live = live
|
||||
self.upcoming = upcoming
|
||||
self.publishedAt = publishedAt
|
||||
self.likes = likes
|
||||
self.dislikes = dislikes
|
||||
self.keywords = keywords
|
||||
}
|
||||
|
||||
init(_ json: JSON) {
|
||||
@ -79,6 +92,15 @@ struct Video: Identifiable, Equatable {
|
||||
live = json["liveNow"].boolValue
|
||||
upcoming = json["isUpcoming"].boolValue
|
||||
|
||||
likes = json["likeCount"].int
|
||||
dislikes = json["dislikeCount"].int
|
||||
|
||||
keywords = json["keywords"].arrayValue.map { $0.stringValue }
|
||||
|
||||
if let publishedInterval = json["published"].double {
|
||||
publishedAt = Date(timeIntervalSince1970: publishedInterval)
|
||||
}
|
||||
|
||||
streams = Video.extractFormatStreams(from: json["formatStreams"].arrayValue)
|
||||
streams.append(contentsOf: Video.extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue))
|
||||
|
||||
@ -103,7 +125,23 @@ struct Video: Identifiable, Equatable {
|
||||
(published.isEmpty || published == "0 seconds ago") ? nil : published
|
||||
}
|
||||
|
||||
var viewsCount: String {
|
||||
var viewsCount: String? {
|
||||
views != 0 ? formattedCount(views) : nil
|
||||
}
|
||||
|
||||
var likesCount: String? {
|
||||
formattedCount(likes)
|
||||
}
|
||||
|
||||
var dislikesCount: String? {
|
||||
formattedCount(dislikes)
|
||||
}
|
||||
|
||||
func formattedCount(_ count: Int!) -> String? {
|
||||
guard count != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = 1
|
||||
@ -111,11 +149,13 @@ struct Video: Identifiable, Equatable {
|
||||
var number: NSNumber
|
||||
var unit: String
|
||||
|
||||
if views < 1_000_000 {
|
||||
number = NSNumber(value: Double(views) / 1000.0)
|
||||
if count < 1000 {
|
||||
return "\(count!)"
|
||||
} else if count < 1_000_000 {
|
||||
number = NSNumber(value: Double(count) / 1000.0)
|
||||
unit = "K"
|
||||
} else {
|
||||
number = NSNumber(value: Double(views) / 1_000_000.0)
|
||||
number = NSNumber(value: Double(count) / 1_000_000.0)
|
||||
unit = "M"
|
||||
}
|
||||
|
||||
|
@ -109,6 +109,17 @@
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||
37B767E02678C5BF0098BAA8 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 37B767DF2678C5BF0098BAA8 /* Logging */; };
|
||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
||||
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
|
||||
37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */; };
|
||||
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */; };
|
||||
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; };
|
||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFE26D2CA3700675966 /* VideoDetails.swift */; };
|
||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
||||
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0126D2CAE700675966 /* PlaybackBar.swift */; };
|
||||
37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
||||
37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
||||
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
|
||||
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */; };
|
||||
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; };
|
||||
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA6269A552E009BE4FB /* Alamofire */; };
|
||||
@ -241,6 +252,11 @@
|
||||
37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsView.swift; sourceTree = "<group>"; };
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
|
||||
37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = "<group>"; };
|
||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
|
||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; };
|
||||
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
|
||||
37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = "<group>"; };
|
||||
37B81B0426D2CEDA00675966 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = "<group>"; };
|
||||
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = "<group>"; };
|
||||
37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarNavigation.swift; sourceTree = "<group>"; };
|
||||
@ -351,8 +367,12 @@
|
||||
371AAE2426CEBA4100901972 /* Player */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37B81B0126D2CAE700675966 /* PlaybackBar.swift */,
|
||||
37BE0BD226A1D4780092E2DB /* Player.swift */,
|
||||
37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */,
|
||||
37B81AFE26D2CA3700675966 /* VideoDetails.swift */,
|
||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
|
||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
|
||||
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
|
||||
);
|
||||
path = Player;
|
||||
@ -543,10 +563,11 @@
|
||||
37D4B1B72672CFE300C925CA /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37141672267A8E10006CA35D /* Country.swift */,
|
||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||
37141672267A8E10006CA35D /* Country.swift */,
|
||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
||||
371F2F19269B43D300E4A7AB /* NavigationState.swift */,
|
||||
37B81B0426D2CEDA00675966 /* PlaybackState.swift */,
|
||||
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
|
||||
376578882685471400D4EA09 /* Playlist.swift */,
|
||||
37C7A1DB267CE9D90010EAD6 /* Profile.swift */,
|
||||
@ -822,6 +843,7 @@
|
||||
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */,
|
||||
3711403F26B206A6005B3555 /* SearchState.swift in Sources */,
|
||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
||||
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37F4AE7226828F0900BD60EA /* VideosCellsView.swift in Sources */,
|
||||
@ -831,6 +853,7 @@
|
||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */,
|
||||
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||
373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */,
|
||||
@ -839,12 +862,14 @@
|
||||
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
377FC7E1267A082600A6BBAF /* ChannelView.swift in Sources */,
|
||||
37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
|
||||
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
|
||||
@ -854,6 +879,7 @@
|
||||
3748186E26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
|
||||
37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
||||
372915E62687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||
37D4B19726717E1500C925CA /* Video.swift in Sources */,
|
||||
371F2F1A269B43D300E4A7AB /* NavigationState.swift in Sources */,
|
||||
@ -876,11 +902,13 @@
|
||||
371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */,
|
||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||
37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */,
|
||||
37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */,
|
||||
3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
373CFAC32696616C003CB2C6 /* CoverSectionRowView.swift in Sources */,
|
||||
@ -890,9 +918,12 @@
|
||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||
377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||
37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */,
|
||||
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||
37F4AE7326828F0900BD60EA /* VideosCellsView.swift in Sources */,
|
||||
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
|
||||
377FC7E0267A082600A6BBAF /* ChannelView.swift in Sources */,
|
||||
379775942689365600DD52A8 /* Array+Next.swift in Sources */,
|
||||
3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */,
|
||||
@ -956,6 +987,7 @@
|
||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */,
|
||||
3765788B2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||
37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */,
|
||||
|
@ -0,0 +1,36 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "extended-gray",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"white" : "0.724"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.328",
|
||||
"green" : "0.328",
|
||||
"red" : "0.325"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.638",
|
||||
"green" : "0.638",
|
||||
"red" : "0.638"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.256",
|
||||
"green" : "0.256",
|
||||
"red" : "0.253"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.537",
|
||||
"green" : "0.522",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.537",
|
||||
"green" : "0.522",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.824",
|
||||
"green" : "0.659",
|
||||
"red" : "0.455"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.824",
|
||||
"green" : "0.659",
|
||||
"red" : "0.455"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.329",
|
||||
"green" : "0.224",
|
||||
"red" : "0.043"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.329",
|
||||
"green" : "0.224",
|
||||
"red" : "0.043"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -26,8 +26,9 @@ struct ContentView: View {
|
||||
.sheet(isPresented: $navigationState.showingVideo) {
|
||||
if let video = navigationState.video {
|
||||
VideoPlayerView(video)
|
||||
|
||||
#if !os(iOS)
|
||||
.frame(minWidth: 590, minHeight: 500)
|
||||
.frame(minWidth: 550, minHeight: 720)
|
||||
.onExitCommand {
|
||||
navigationState.showingVideo = false
|
||||
}
|
||||
|
55
Shared/Player/PlaybackBar.swift
Normal file
55
Shared/Player/PlaybackBar.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
132
Shared/Player/VideoDetails.swift
Normal file
132
Shared/Player/VideoDetails.swift
Normal 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)
|
||||
}
|
||||
}
|
42
Shared/Player/VideoDetailsPaddingModifier.swift
Normal file
42
Shared/Player/VideoDetailsPaddingModifier.swift
Normal 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)
|
||||
}
|
||||
}
|
70
Shared/Player/VideoPlayerSizeModifier.swift
Normal file
70
Shared/Player/VideoPlayerSizeModifier.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
#if !os(tvOS)
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(video.title)
|
||||
Text(video.author)
|
||||
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
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)
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#elseif os(macOS)
|
||||
PlaybackBar(playbackState: playbackState, video: video)
|
||||
#endif
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ struct VideoView: View {
|
||||
|
||||
if video.views != 0 {
|
||||
Image(systemName: "eye")
|
||||
Text(video.viewsCount)
|
||||
Text(video.viewsCount!)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
@ -139,7 +139,7 @@ struct VideoView: View {
|
||||
|
||||
var thumbnail: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
thumbnailImage(quality: .maxres)
|
||||
thumbnailImage(quality: .maxresdefault)
|
||||
|
||||
VStack {
|
||||
HStack(alignment: .top) {
|
||||
@ -181,12 +181,13 @@ struct VideoView: View {
|
||||
ProgressView()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
}
|
||||
.mask(RoundedRectangle(cornerRadius: 12))
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.square")
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 320, maxWidth: .infinity, minHeight: 180, maxHeight: .infinity)
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 180, maxHeight: .infinity)
|
||||
.background(.gray)
|
||||
.mask(RoundedRectangle(cornerRadius: 12))
|
||||
#if os(tvOS)
|
||||
.frame(minHeight: layout == .cells ? 320 : 200)
|
||||
#endif
|
||||
|
@ -1,10 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
struct Player: NSViewControllerRepresentable {
|
||||
@ObservedObject var playbackState: PlaybackState
|
||||
var video: Video!
|
||||
|
||||
func makeNSViewController(context _: Context) -> PlayerViewController {
|
||||
let controller = PlayerViewController()
|
||||
|
||||
controller.playbackState = playbackState
|
||||
controller.video = video
|
||||
|
||||
return controller
|
||||
|
@ -5,7 +5,8 @@ final class PlayerViewController: NSViewController {
|
||||
var video: Video!
|
||||
|
||||
var player = AVPlayer()
|
||||
var playerState: PlayerState! = PlayerState()
|
||||
var playerState: PlayerState!
|
||||
var playbackState: PlaybackState!
|
||||
var playerView = AVPlayerView()
|
||||
|
||||
override func viewDidDisappear() {
|
||||
@ -19,6 +20,9 @@ final class PlayerViewController: NSViewController {
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
playerState = PlayerState()
|
||||
playerState.playbackState = playbackState
|
||||
|
||||
guard playerState.player == nil else {
|
||||
return
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user