From 175526cc0522c5040fcd5560d7e3e914f0c7539a Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Sat, 26 Jun 2021 18:19:01 +1000 Subject: [PATCH] Add up next view --- JellyfinPlayer.xcodeproj/project.pbxproj | 4 + JellyfinPlayer/VideoPlayer.storyboard | 11 ++- JellyfinPlayer/VideoPlayer.swift | 93 +++++++++++++++--------- JellyfinPlayer/VideoUpNextView.swift | 85 ++++++++++++++++++++++ 4 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 JellyfinPlayer/VideoUpNextView.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 2424e3dd..5450a054 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; + 0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */; }; 531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */; }; 531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069512684E7EE00CFFDBA /* MediaInfoView.swift */; }; 531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069522684E7EE00CFFDBA /* SubtitlesView.swift */; }; @@ -203,6 +204,7 @@ 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = ""; }; 091B5A882683142E00D78B61 /* UDPBroadCastConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UDPBroadCastConnection.swift; sourceTree = ""; }; 09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = ""; }; + 0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUpNextView.swift; sourceTree = ""; }; 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = ""; }; 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 531069502684E7EE00CFFDBA /* InfoTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoTabBarViewController.swift; sourceTree = ""; }; @@ -536,6 +538,7 @@ 53987CA526572F0700E7EA70 /* SeriesItemView.swift */, 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, + 0959A5FC2686D29800C7C9A9 /* VideoUpNextView.swift */, 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */, 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, @@ -1002,6 +1005,7 @@ 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, + 0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */, 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, diff --git a/JellyfinPlayer/VideoPlayer.storyboard b/JellyfinPlayer/VideoPlayer.storyboard index a4c0ad6c..7c734277 100644 --- a/JellyfinPlayer/VideoPlayer.storyboard +++ b/JellyfinPlayer/VideoPlayer.storyboard @@ -1,9 +1,8 @@ - + - - + @@ -170,6 +169,11 @@ + @@ -194,6 +198,7 @@ + diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 9b998476..5e64a910 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -32,6 +32,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var cancellables = Set() var mediaPlayer = VLCMediaPlayer() + @IBOutlet weak var upNextView: UIView! @IBOutlet weak var timeText: UILabel! @IBOutlet weak var videoContentView: UIView! @IBOutlet weak var videoControlsView: UIView! @@ -79,7 +80,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var remoteTimeUpdateTimer: Timer? - + + var smallView : CGRect = .zero + var largeView : CGRect = .zero + var upNextViewModel: UpNextViewModel = UpNextViewModel() + // MARK: IBActions @IBAction func seekSliderStart(_ sender: Any) { if playerDestination == .local { @@ -142,6 +147,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBAction func controlViewTapped(_ sender: Any) { if playerDestination == .local { videoControlsView.isHidden = true + if manifest.type == "Episode" { + smallNextUpView() + } } } @@ -149,6 +157,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if playerDestination == .local { videoControlsView.isHidden = false controlsAppearTime = CACurrentMediaTime() + if manifest.type == "Episode" { + largeNextUpView() + } } } @@ -381,38 +392,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe titleLabel.text = manifest.name ?? "" } else { titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”" - print("ep count \(manifest.episodeCount) current \(manifest.indexNumber) end \(manifest.indexNumberEnd)") - - -// TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, season: manifest.parentIndexNumber ?? 0, startIndex: manifest.indexNumber, limit: 1) -// .sink(receiveCompletion: { completion in -// print(completion) -// }, receiveValue: { response in -// if let item = response.items?.first { -// print(item.name, item.indexNumber) -// } -// }) -// .store(in: &cancellables) - -// TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, season: manifest.parentIndexNumber ?? 0, startIndex: manifest.indexNumber, limit: 1) -// .sink(receiveCompletion: { completion in -// print(completion) -// }, receiveValue: { response in -// if let item = response.items?.first { -// print(item.name, item.indexNumber) -// } -// }) -// .store(in: &cancellables) -// -// TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, startIndex: manifest.indexNumber, limit: 1, seriesId: manifest.seriesId) -// .sink(receiveCompletion: { completion in -// print(completion) -// }, receiveValue: { response in -// print(response.items) -// }) -// .store(in: &cancellables) -// + setupNextUpView() } if !UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat { @@ -697,6 +678,43 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe selectedPlaybackSpeedIndex = index mediaPlayer.rate = playbackSpeeds[index] } + + func smallNextUpView() { + upNextViewModel.largeView = false + upNextView.frame = smallView + } + + func largeNextUpView() { + upNextViewModel.largeView = true + upNextView.frame = largeView + } + + func setupNextUpView() { + // Get next episode item + TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, season: manifest.parentIndexNumber ?? 0, startIndex: manifest.indexNumber, limit: 1) + .sink(receiveCompletion: { completion in + print(completion) + }, receiveValue: { [self] response in + if let item = response.items?.first { + self.upNextViewModel.item = item + } + }) + .store(in: &cancellables) + + // Create the swiftUI view + let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel)) + self.upNextView.addSubview(contentView.view) + contentView.view.backgroundColor = .clear + contentView.view.translatesAutoresizingMaskIntoConstraints = false + contentView.view.topAnchor.constraint(equalTo: upNextView.topAnchor).isActive = true + contentView.view.bottomAnchor.constraint(equalTo: upNextView.bottomAnchor).isActive = true + contentView.view.leftAnchor.constraint(equalTo: upNextView.leftAnchor).isActive = true + contentView.view.rightAnchor.constraint(equalTo: upNextView.rightAnchor).isActive = true + + // Frame sizes depend on if controls are hidden or shown + smallView = upNextView.frame + largeView = CGRect(x: 500, y: 90, width: 400, height: 270) + } } // MARK: - GCKGenericChannelDelegate @@ -892,12 +910,21 @@ extension PlayerViewController: VLCMediaPlayerDelegate { mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) seekSlider.setValue(mediaPlayer.position, animated: true) delegate?.hideLoadingView(self) - + + if manifest.type == "Episode" && upNextViewModel.item != nil{ + if time > 0.95 { + upNextView.isHidden = false + } else { + upNextView.isHidden = true + } + } + timeText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst()) if CACurrentMediaTime() - controlsAppearTime > 5 { UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { self.videoControlsView.alpha = 0.0 + self.smallNextUpView() }, completion: { (_: Bool) in self.videoControlsView.isHidden = true self.videoControlsView.alpha = 1 diff --git a/JellyfinPlayer/VideoUpNextView.swift b/JellyfinPlayer/VideoUpNextView.swift new file mode 100644 index 00000000..f3dd5771 --- /dev/null +++ b/JellyfinPlayer/VideoUpNextView.swift @@ -0,0 +1,85 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import JellyfinAPI + +class UpNextViewModel: ObservableObject { + @Published var largeView: Bool = false + @Published var item: BaseItemDto? = nil + + func episodeAndSeasonNumber() -> String { + if let pID = item?.parentIndexNumber, let id = item?.indexNumber { + return "S\(pID):E\(id)" + } + return "" + } + + func episodeName() -> String { + if let name = item?.name { + return name + } + return "" + } +} + +struct VideoUpNextView: View { + + @ObservedObject var viewModel: UpNextViewModel + + var body: some View { + + Button(action: { + print("Next episode") + }, label: { + + VStack(alignment: viewModel.largeView ? .leading : .center) { + Text("Up Next") + .foregroundColor(.white) + .font(viewModel.largeView ? .title : .body) + + image + + if viewModel.largeView { + Text(viewModel.episodeName()) + .padding(.trailing, 50) + .foregroundColor(.white) + .font(.title) + .lineLimit(1) + + } + } + + }) + } + + var image : some View { + if let url = viewModel.item?.getPrimaryImage(maxWidth: 100) { + return AnyView( + ImageView(src: url) + .frame(maxWidth: .infinity) + .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fit) + .overlay(overlayIndicator, alignment: .topTrailing)) + } + else { + return AnyView(EmptyView()) + } + } + + var overlayIndicator : some View { + Text(viewModel.episodeAndSeasonNumber()) + .font(viewModel.largeView ? .title3 : .body) + .foregroundColor(.white) + .padding(.horizontal, 5) + .background(Color.black.opacity(0.6)) + .cornerRadius(5) + .padding(5) + + } +}