From f80b61f9c7883d2d91be88cd03cf24df673bf4bc Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 22 Aug 2021 21:13:33 +0200 Subject: [PATCH] Display more details in player view --- Fixtures/Video+Fixtures.swift | 10 +- Model/PlaybackState.swift | 25 ++++ Model/PlayerState.swift | 18 ++- Model/Video.swift | 50 ++++++- Pearvidious.xcodeproj/project.pbxproj | 34 ++++- .../Contents.json | 36 +++++ .../Contents.json | 38 +++++ .../Contents.json | 38 +++++ .../Contents.json | 38 +++++ .../Contents.json | 38 +++++ Shared/Navigation/ContentView.swift | 3 +- Shared/Player/PlaybackBar.swift | 55 ++++++++ Shared/Player/Player.swift | 3 + Shared/Player/PlayerViewController.swift | 13 +- Shared/Player/VideoDetails.swift | 132 ++++++++++++++++++ .../Player/VideoDetailsPaddingModifier.swift | 42 ++++++ Shared/Player/VideoPlayerSizeModifier.swift | 70 ++++++++++ Shared/Player/VideoPlayerView.swift | 89 ++++++++++-- Shared/Playlists/PlaylistFormView.swift | 2 +- Shared/Videos/VideoView.swift | 9 +- macOS/Player.swift | 3 + macOS/PlayerViewController.swift | 6 +- 22 files changed, 716 insertions(+), 36 deletions(-) create mode 100644 Model/PlaybackState.swift create mode 100644 Shared/Assets.xcassets/VideoDetailBackgroundColor.colorset/Contents.json create mode 100644 Shared/Assets.xcassets/VideoDetailBorderColor.colorset/Contents.json create mode 100644 Shared/Assets.xcassets/VideoDetailDislikesSymbolColor.colorset/Contents.json create mode 100644 Shared/Assets.xcassets/VideoDetailLikesSymbolColor.colorset/Contents.json create mode 100644 Shared/Assets.xcassets/VideoDetailViewsBackgroundColor.colorset/Contents.json create mode 100644 Shared/Player/PlaybackBar.swift create mode 100644 Shared/Player/VideoDetails.swift create mode 100644 Shared/Player/VideoDetailsPaddingModifier.swift create mode 100644 Shared/Player/VideoPlayerSizeModifier.swift diff --git a/Fixtures/Video+Fixtures.swift b/Fixtures/Video+Fixtures.swift index 2a44bc2a..1662c0ec 100644 --- a/Fixtures/Video+Fixtures.swift +++ b/Fixtures/Video+Fixtures.swift @@ -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"] ) } diff --git a/Model/PlaybackState.swift b/Model/PlaybackState.swift new file mode 100644 index 00000000..df017b9d --- /dev/null +++ b/Model/PlaybackState.swift @@ -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 + } +} diff --git a/Model/PlayerState.swift b/Model/PlayerState.swift index fbcc8ec8..bbcb6f30 100644 --- a/Model/PlayerState.swift +++ b/Model/PlayerState.swift @@ -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() } } diff --git a/Model/Video.swift b/Model/Video.swift index 8f4056f7..8aaa5b03 100644 --- a/Model/Video.swift +++ b/Model/Video.swift @@ -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" } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 7995049c..deac8d04 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -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 = ""; }; 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = ""; }; 37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = ""; }; + 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = ""; }; + 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = ""; }; + 37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = ""; }; + 37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = ""; }; + 37B81B0426D2CEDA00675966 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = ""; }; 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = ""; }; 37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarNavigation.swift; sourceTree = ""; }; @@ -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 */, diff --git a/Shared/Assets.xcassets/VideoDetailBackgroundColor.colorset/Contents.json b/Shared/Assets.xcassets/VideoDetailBackgroundColor.colorset/Contents.json new file mode 100644 index 00000000..1533dbef --- /dev/null +++ b/Shared/Assets.xcassets/VideoDetailBackgroundColor.colorset/Contents.json @@ -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 + } +} diff --git a/Shared/Assets.xcassets/VideoDetailBorderColor.colorset/Contents.json b/Shared/Assets.xcassets/VideoDetailBorderColor.colorset/Contents.json new file mode 100644 index 00000000..0af8244c --- /dev/null +++ b/Shared/Assets.xcassets/VideoDetailBorderColor.colorset/Contents.json @@ -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 + } +} diff --git a/Shared/Assets.xcassets/VideoDetailDislikesSymbolColor.colorset/Contents.json b/Shared/Assets.xcassets/VideoDetailDislikesSymbolColor.colorset/Contents.json new file mode 100644 index 00000000..0a170084 --- /dev/null +++ b/Shared/Assets.xcassets/VideoDetailDislikesSymbolColor.colorset/Contents.json @@ -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 + } +} diff --git a/Shared/Assets.xcassets/VideoDetailLikesSymbolColor.colorset/Contents.json b/Shared/Assets.xcassets/VideoDetailLikesSymbolColor.colorset/Contents.json new file mode 100644 index 00000000..4025ebe7 --- /dev/null +++ b/Shared/Assets.xcassets/VideoDetailLikesSymbolColor.colorset/Contents.json @@ -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 + } +} diff --git a/Shared/Assets.xcassets/VideoDetailViewsBackgroundColor.colorset/Contents.json b/Shared/Assets.xcassets/VideoDetailViewsBackgroundColor.colorset/Contents.json new file mode 100644 index 00000000..a1ba12e1 --- /dev/null +++ b/Shared/Assets.xcassets/VideoDetailViewsBackgroundColor.colorset/Contents.json @@ -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 + } +} diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index a4cf0f36..e3f4b7df 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -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 } diff --git a/Shared/Player/PlaybackBar.swift b/Shared/Player/PlaybackBar.swift new file mode 100644 index 00000000..489e4b3a --- /dev/null +++ b/Shared/Player/PlaybackBar.swift @@ -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) + } +} diff --git a/Shared/Player/Player.swift b/Shared/Player/Player.swift index acd04e9f..417c0a4f 100644 --- a/Shared/Player/Player.swift +++ b/Shared/Player/Player.swift @@ -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 diff --git a/Shared/Player/PlayerViewController.swift b/Shared/Player/PlayerViewController.swift index ff572bb8..95645d90 100644 --- a/Shared/Player/PlayerViewController.swift +++ b/Shared/Player/PlayerViewController.swift @@ -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 } } } diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift new file mode 100644 index 00000000..4f2c6c17 --- /dev/null +++ b/Shared/Player/VideoDetails.swift @@ -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) + } +} diff --git a/Shared/Player/VideoDetailsPaddingModifier.swift b/Shared/Player/VideoDetailsPaddingModifier.swift new file mode 100644 index 00000000..19f100c7 --- /dev/null +++ b/Shared/Player/VideoDetailsPaddingModifier.swift @@ -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) + } +} diff --git a/Shared/Player/VideoPlayerSizeModifier.swift b/Shared/Player/VideoPlayerSizeModifier.swift new file mode 100644 index 00000000..ad83c0df --- /dev/null +++ b/Shared/Player/VideoPlayerSizeModifier.swift @@ -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 + } +} diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 2525e6a5..41b1b4b8 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -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 private var navigationState @ObservedObject private var store = Store