diff --git a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift index 29b6aec4..49adce26 100644 --- a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift +++ b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift @@ -43,7 +43,7 @@ struct LandscapeItemElement: View { var body: some View { VStack { - ImageView(src: (item.type == "Episode" ? item.getSeriesBackdropImage(maxWidth: 445) : item.getBackdropImage(maxWidth: 445)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash()) + ImageView(src: (item.type == "Episode" ? item.getSeriesBackdropImage(maxWidth: 800) : item.getBackdropImage(maxWidth: 800)), bh: item.type == "Episode" ? item.getSeriesBackdropImageBlurHash() : item.getBackdropImageBlurHash()) .frame(width: 445, height: 250) .cornerRadius(10) .overlay( @@ -97,7 +97,7 @@ struct LandscapeItemElement: View { if envFocus == true { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // your code here - if self.focused == true { + if focused == true { backgroundURL = item.getBackdropImage(maxWidth: 1080) BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash()) } diff --git a/JellyfinPlayer tvOS/Components/PlainLinkButton.swift b/JellyfinPlayer tvOS/Components/PlainLinkButton.swift new file mode 100644 index 00000000..5f680fc0 --- /dev/null +++ b/JellyfinPlayer tvOS/Components/PlainLinkButton.swift @@ -0,0 +1,29 @@ +// + /* + * 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 + +struct PlainLinkButton: View { + @Environment(\.isFocused) var envFocused: Bool + @State var focused: Bool = false + @State var label: String + + var body: some View { + Text(label) + .fontWeight(focused ? .bold : .regular) + .foregroundColor(.blue) + .onChange(of: envFocused) { envFocus in + withAnimation(.linear(duration: 0.15)) { + self.focused = envFocus + } + } + .scaleEffect(focused ? 1.1 : 1) + } +} diff --git a/JellyfinPlayer tvOS/Components/PortraitItemElement.swift b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift index c8e12201..86c0095d 100644 --- a/JellyfinPlayer tvOS/Components/PortraitItemElement.swift +++ b/JellyfinPlayer tvOS/Components/PortraitItemElement.swift @@ -19,7 +19,7 @@ struct PortraitItemElement: View { var body: some View { VStack { - ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200), bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash()) + ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 400) : item.getPrimaryImage(maxWidth: 400), bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash()) .frame(width: 200, height: 300) .cornerRadius(10) .shadow(radius: focused ? 10.0 : 0) @@ -65,7 +65,7 @@ struct PortraitItemElement: View { if envFocus == true { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // your code here - if self.focused == true { + if focused == true { backgroundURL = item.getBackdropImage(maxWidth: 1080) BackgroundManager.current.setBackground(to: backgroundURL!, hash: item.getBackdropImageBlurHash()) } diff --git a/JellyfinPlayer tvOS/ItemView.swift b/JellyfinPlayer tvOS/ItemView.swift index 36ed9877..68ead546 100644 --- a/JellyfinPlayer tvOS/ItemView.swift +++ b/JellyfinPlayer tvOS/ItemView.swift @@ -27,21 +27,14 @@ struct ItemView: View { } var body: some View { - ZStack { - NavigationLink(destination: VideoPlayerView(item: videoPlayerItem.itemToPlay), isActive: $videoPlayerItem.shouldShowPlayer) { - EmptyView() + Group { + if item.type == "Movie" { + MovieItemView(viewModel: .init(item: item)) + } else if item.type == "Series" { + SeriesItemView(viewModel: .init(item: item)) + } else { + Text("Type: \(item.type ?? "") not implemented yet :(") } - .buttonStyle(PlainNavigationLinkButtonStyle()) - .focusable(false) - - Group { - if item.type == "Movie" { - MovieItemView(item: item) - } else { - Text("Type: \(item.type ?? "") not implemented yet :(") - } - } - .environmentObject(videoPlayerItem) } } } diff --git a/JellyfinPlayer tvOS/MovieItemView.swift b/JellyfinPlayer tvOS/MovieItemView.swift index deb0de44..e697110d 100644 --- a/JellyfinPlayer tvOS/MovieItemView.swift +++ b/JellyfinPlayer tvOS/MovieItemView.swift @@ -11,9 +11,8 @@ import SwiftUI import JellyfinAPI struct MovieItemView: View { - let item: BaseItemDto - @EnvironmentObject private var playbackInfo: VideoPlayerItem - + @ObservedObject var viewModel: MovieItemViewModel + @State var actors: [BaseItemPerson] = []; @State var studio: String? = nil; @State var director: String? = nil; @@ -25,9 +24,9 @@ struct MovieItemView: View { director = nil studio = nil var actor_index = 0; - item.people?.forEach { person in + viewModel.item.people?.forEach { person in if(person.type == "Actor") { - if(actor_index < 8) { + if(actor_index < 4) { actors.append(person) } actor_index = actor_index + 1; @@ -36,141 +35,153 @@ struct MovieItemView: View { director = person.name ?? "" } } + + studio = viewModel.item.studios?.first?.name ?? nil } var body: some View { ZStack { - ImageView(src: item.getBackdropImage(maxWidth: 1920), bh: item.getBackdropImageBlurHash()) + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash()) .opacity(0.4) ScrollView { - LazyVStack { + LazyVStack(alignment: .leading) { + Spacer() //i hate ficus engine + .frame(width: 1920, height: 2) + .focusable() + Text(viewModel.item.name ?? "") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) HStack { - VStack(alignment: .leading) { - Text(item.name ?? "") - .font(.title) - .fontWeight(.bold) - .foregroundColor(.primary) - HStack { - if item.productionYear != nil { - Text(String(item.productionYear!)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - Text(item.getItemRuntime()).font(.subheadline) - .fontWeight(.medium) + if viewModel.item.productionYear != nil { + Text(String(viewModel.item.productionYear!)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + Text(viewModel.item.getItemRuntime()).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if viewModel.item.officialRating != nil { + Text(viewModel.item.officialRating!).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + } + + HStack { + VStack(alignment: .trailing) { + if(studio != nil) { + Text("STUDIO") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(studio!) + .font(.body) + .fontWeight(.semibold) .foregroundColor(.secondary) - .lineLimit(1) - if item.officialRating != nil { - Text(item.officialRating!).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) - } + .padding(.bottom, 40) } - HStack { - VStack(alignment: .trailing) { - if(studio != nil) { - Text("STUDIO") - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(studio!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if(director != nil) { - Text("DIRECTOR") - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - Text(director!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .padding(.bottom, 40) - } - - if(!actors.isEmpty) { - Text("CAST") - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.primary) - ForEach(actors, id: \.id) { person in - Text(person.name!) - .font(.body) - .fontWeight(.semibold) - .foregroundColor(.secondary) - } - } - Spacer() - } - VStack(alignment: .leading) { - Text(item.taglines?.first ?? "") + if(director != nil) { + Text("DIRECTOR") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(director!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(.bottom, 40) + } + + if(!actors.isEmpty) { + Text("CAST") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + ForEach(actors, id: \.id) { person in + Text(person.name!) .font(.body) - .italic() - .fontWeight(.medium) - .foregroundColor(.primary) - - Text(item.overview ?? "") - .font(.body) - .fontWeight(.medium) - .foregroundColor(.primary) - - HStack { - VStack { - Button { - playbackInfo.shouldShowPlayer = true - } label: { - Image(systemName: "heart.fill") - .font(.system(size: 40)) - .padding(.vertical, 12).padding(.horizontal, 20) - } - Text("Favorite") - .font(.caption) - } - VStack { - Button { - playbackInfo.itemToPlay = item - playbackInfo.shouldShowPlayer = true - } label: { - Image(systemName: "play.fill") - .font(.system(size: 40)) - .padding(.vertical, 12).padding(.horizontal, 20) - }.prefersDefaultFocus(in: namespace) - Text("Play") - .font(.caption) - } - VStack { - Button { - playbackInfo.shouldShowPlayer = true - } label: { - Image(systemName: "eye.fill") - .font(.system(size: 40)) - .padding(.vertical, 12).padding(.horizontal, 20) - } - Text("Mark Watched") - .font(.caption) - } - }.padding(.top, 15) - Spacer() + .fontWeight(.semibold) + .foregroundColor(.secondary) } - }.padding(.top, 50) - } - - VStack { - ImageView(src: item.getPrimaryImage(maxWidth: 450), bh: item.getPrimaryImageBlurHash()) - .frame(width: 450, height: 675) - .cornerRadius(10) + } Spacer() } + VStack(alignment: .leading) { + if(!(viewModel.item.taglines ?? []).isEmpty) { + Text(viewModel.item.taglines?.first ?? "") + .font(.body) + .italic() + .fontWeight(.medium) + .foregroundColor(.primary) + } + Text(viewModel.item.overview ?? "") + .font(.body) + .fontWeight(.medium) + .foregroundColor(.primary) + + HStack { + VStack { + Button { + viewModel.updateFavoriteState() + } label: { + Image(systemName: "heart.fill") + .foregroundColor(viewModel.isFavorited ? .red : .primary) + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + } + Text(viewModel.isFavorited ? "Unfavorite" : "Favorite") + .font(.caption) + } + VStack { + NavigationLink(destination: VideoPlayerView(item: viewModel.item)) { + Image(systemName: "play.fill") + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + } + Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : "Play") + .font(.caption) + } + VStack { + Button { + viewModel.updateWatchState() + } label: { + Image(systemName: "eye.fill") + .foregroundColor(viewModel.isWatched ? .red : .primary) + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + } + Text(viewModel.isWatched ? "Unwatch" : "Mark Watched") + .font(.caption) + } + }.padding(.top, 15) + Spacer() + } + }.padding(.top, 50) + + if(!viewModel.similarItems.isEmpty) { + Text("More Like This") + .font(.headline) + .fontWeight(.semibold) + ScrollView(.horizontal) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(viewModel.similarItems, id: \.id) { similarItems in + NavigationLink(destination: ItemView(item: similarItems)) { + PortraitItemElement(item: similarItems) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90)) + .frame(height: 360) } }.padding(EdgeInsets(top: 90, leading: 90, bottom: 0, trailing: 90)) } diff --git a/JellyfinPlayer tvOS/SeriesItemView.swift b/JellyfinPlayer tvOS/SeriesItemView.swift new file mode 100644 index 00000000..d2341ab3 --- /dev/null +++ b/JellyfinPlayer tvOS/SeriesItemView.swift @@ -0,0 +1,227 @@ +// + /* + * 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 + +struct SeriesItemView: View { + @ObservedObject var viewModel: SeriesItemViewModel + + @State var actors: [BaseItemPerson] = []; + @State var studio: String? = nil; + @State var director: String? = nil; + + @Environment(\.resetFocus) var resetFocus + @Namespace private var namespace + + func onAppear() { + actors = [] + director = nil + studio = nil + var actor_index = 0; + viewModel.item.people?.forEach { person in + if(person.type == "Actor") { + if(actor_index < 4) { + actors.append(person) + } + actor_index = actor_index + 1; + } + if(person.type == "Director") { + director = person.name ?? "" + } + } + + studio = viewModel.item.studios?.first?.name ?? nil + } + + var body: some View { + ZStack { + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash()) + .opacity(0.4) + ScrollView { + ScrollViewReader { reader in + LazyVStack(alignment: .leading) { + Spacer() //i hate ficus engine + .frame(width: 1920, height: 2) + .focusable() + Text(viewModel.item.name ?? "") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.primary) + HStack { + Text(viewModel.getRunYears()).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if viewModel.item.officialRating != nil { + Text(viewModel.item.officialRating!).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + if viewModel.item.communityRating != nil { + HStack { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .font(.subheadline) + Text(String(viewModel.item.communityRating!)).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + } + + HStack { + VStack(alignment: .trailing) { + if(studio != nil) { + Text("STUDIO") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(studio!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(.bottom, 40) + } + + if(director != nil) { + Text("DIRECTOR") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + Text(director!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(.bottom, 40) + } + + if(!actors.isEmpty) { + Text("CAST") + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + ForEach(actors, id: \.id) { person in + Text(person.name!) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.secondary) + } + } + Spacer() + } + VStack(alignment: .leading) { + if(!(viewModel.item.taglines ?? []).isEmpty) { + Text(viewModel.item.taglines?.first ?? "") + .font(.body) + .italic() + .fontWeight(.medium) + .foregroundColor(.primary) + } + Text(viewModel.item.overview ?? "") + .font(.body) + .fontWeight(.medium) + .foregroundColor(.primary) + + HStack { + VStack { + Button { + viewModel.updateFavoriteState() + } label: { + Image(systemName: "heart.fill") + .foregroundColor(viewModel.isFavorited ? .red : .primary) + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + }.prefersDefaultFocus(in: namespace) + Text(viewModel.isFavorited ? "Unfavorite" : "Favorite") + .font(.caption) + } + if(viewModel.nextUpItem != nil) { + VStack { + NavigationLink(destination: VideoPlayerView(item: viewModel.nextUpItem!)) { + Image(systemName: "play.fill") + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + } + Text("Play • \(viewModel.nextUpItem!.getEpisodeLocator())") + .font(.caption) + } + } + VStack { + Button { + viewModel.updateWatchState() + } label: { + Image(systemName: "eye.fill") + .foregroundColor(viewModel.isWatched ? .red : .primary) + .font(.system(size: 40)) + .padding(.vertical, 12).padding(.horizontal, 20) + } + Text(viewModel.isWatched ? "Unwatch" : "Mark Watched") + .font(.caption) + } + }.padding(.top, 15) + Spacer() + } + }.padding(.top, 50) + + if(viewModel.nextUpItem != nil) { + Text("Next Up") + .font(.headline) + .fontWeight(.semibold) + NavigationLink(destination: ItemView(item: viewModel.nextUpItem!)) { + LandscapeItemElement(item: viewModel.nextUpItem!) + }.buttonStyle(PlainNavigationLinkButtonStyle()).padding(.bottom, 1) + } + + if(!viewModel.seasons.isEmpty) { + Text("Seasons") + .font(.headline) + .fontWeight(.semibold) + ScrollView(.horizontal) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(viewModel.seasons, id: \.id) { season in + NavigationLink(destination: ItemView(item: season)) { + PortraitItemElement(item: season) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90)) + .frame(height: 360) + } + + if(!viewModel.similarItems.isEmpty) { + Text("More Like This") + .font(.headline) + .fontWeight(.semibold) + ScrollView(.horizontal) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(viewModel.similarItems, id: \.id) { similarItems in + NavigationLink(destination: ItemView(item: similarItems)) { + PortraitItemElement(item: similarItems) + }.buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.padding(EdgeInsets(top: -30, leading: -90, bottom: 0, trailing: -90)) + .frame(height: 360) + } + }.padding(EdgeInsets(top: 90, leading: 90, bottom: 45, trailing: 90)) + } + }.focusScope(namespace) + }.onAppear(perform: onAppear) + } +} diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard index eee0e161..9f7b5970 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.storyboard @@ -1,8 +1,9 @@ - + - + + @@ -38,6 +39,9 @@ + + + + + + diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 581442ba..657bbf41 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -49,16 +49,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, var lastTime: Float = 0.0 var startTime: Int = 0 - var selectedAudioTrack: Int32 = -1 { - didSet { - print(selectedAudioTrack) - } - } - var selectedCaptionTrack: Int32 = -1 { - didSet { - print(selectedCaptionTrack) - } - } + var selectedAudioTrack: Int32 = -1 + var selectedCaptionTrack: Int32 = -1 var subtitleTrackArray: [Subtitle] = [] var audioTrackArray: [AudioTrack] = [] @@ -86,7 +78,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, // Check if focused on the tab bar, allows for swipe up to dismiss the info panel if context.nextFocusedView!.description.contains("UITabBarButton") { // Set value after half a second so info panel is not dismissed instantly when swiping up from content - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { self.focusedOnTabBar = true } } else { @@ -103,6 +95,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView + mediaPlayer.libraryInstance.debugLogging = true; if let runTimeTicks = manifest.runTimeTicks { videoDuration = Double(runTimeTicks / 10_000_000) @@ -132,9 +125,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, transportBarView.layer.cornerRadius = CGFloat(5) setupGestures() - fetchVideo() - setupNowPlayingCC() // Adjust subtitle size @@ -321,13 +312,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } } -// commandCenter.enableLanguageOptionCommand.addTarget { [weak self](remoteEvent) in -// guard let self = self else {return .commandFailed} -// -// -// -// } - var runTicks = 0 var playbackTicks = 0 @@ -376,12 +360,11 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } - // Grabs a refference to the info panel view controller + // Grabs a reference to the info panel view controller override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "infoView" { containerViewController = segue.destination as? InfoTabBarViewController containerViewController?.videoPlayer = self - } } @@ -403,7 +386,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, self.sendProgressReport(eventName: "pause") self.updateNowPlayingCenter(time: nil, playing: false) - + self.toggleInfoContainer() animateScrubber() self.scrubLabel.frame = CGRect(x: self.scrubberView.frame.minX - self.scrubLabel.frame.width/2, y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) @@ -414,9 +397,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, mediaPlayer.play() self.updateNowPlayingCenter(time: nil, playing: true) - + self.toggleInfoContainer() self.sendProgressReport(eventName: "unpause") - animateScrubber() } @@ -463,15 +445,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.userPanned(panGestureRecognizer:))) view.addGestureRecognizer(panGestureRecognizer) - - let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:))) - swipeRecognizer.direction = .right - view.addGestureRecognizer(swipeRecognizer) - - let swipeRecognizerl = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:))) - swipeRecognizerl.direction = .left - view.addGestureRecognizer(swipeRecognizerl) - } @objc func backButtonPressed(tap: UITapGestureRecognizer) { @@ -509,7 +482,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, let velocity = panGestureRecognizer.velocity(in: view) // Swiped up - Handle dismissing info panel - if translation.y < -700 && (focusedOnTabBar && showingInfoPanel) { + if translation.y < -400 && (focusedOnTabBar && showingInfoPanel) { toggleInfoContainer() return } @@ -519,7 +492,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } // Swiped down - Show the info panel - if translation.y > 700 { + if translation.y > 400 { toggleInfoContainer() return } @@ -549,40 +522,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } - // Not currently used - @objc func swipe(swipe: UISwipeGestureRecognizer!) { - print("swiped") - switch swipe.direction { - case .left: - print("swiped left") -// mediaPlayer.pause() - // player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1)) -// mediaPlayer.play() - case .right: - print("swiped right") -// mediaPlayer.pause() - // player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1)) -// mediaPlayer.play() - case .up: - break - case .down: - break - default: - break - } - - } - /// Play/Pause or Select is pressed on the AppleTV remote @objc func selectButtonTapped() { + print("select") if loading { return } - showingControls = true - controlsView.isHidden = false - controlsAppearTime = CACurrentMediaTime() - // Move to seeked position if seeking { scrubLabel.isHidden = true @@ -787,12 +733,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, return text.hasPrefix("0") && text.count > 4 ? .init(text.dropFirst()) : text } - - // When VLC video starts playing a real device can no longer receive gesture recognisers, adding this in hopes to fix the issue but no luck - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - print("recognisesimultaneousvideoplayer") - return true - } } extension Comparable { diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 042fb487..15794b83 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069542684E7EE00CFFDBA /* AudioView.swift */; }; 5310695C2684E7EE00CFFDBA /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */; }; 5310695D2684E7EE00CFFDBA /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */; }; + 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A16268B919A003024C9 /* SeriesItemView.swift */; }; + 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53116A18268B947A003024C9 /* PlainLinkButton.swift */; }; 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; @@ -218,6 +220,8 @@ 531069542684E7EE00CFFDBA /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = ""; }; 531069552684E7EE00CFFDBA /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = ""; }; 531069562684E7EE00CFFDBA /* VideoPlayer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = ""; }; + 53116A16268B919A003024C9 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; + 53116A18268B947A003024C9 /* PlainLinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainLinkButton.swift; sourceTree = ""; }; 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; @@ -458,6 +462,7 @@ 53A83C32268A309300DF3D92 /* LibraryView.swift */, 53CD2A3F268A49C2002ABD4E /* ItemView.swift */, 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, + 53116A16268B919A003024C9 /* SeriesItemView.swift */, ); path = "JellyfinPlayer tvOS"; sourceTree = ""; @@ -497,6 +502,7 @@ 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, 536D3D80267BDFC60004248C /* PortraitItemElement.swift */, 536D3D87267C17350004248C /* PublicUserButton.swift */, + 53116A18268B947A003024C9 /* PlainLinkButton.swift */, ); path = Components; sourceTree = ""; @@ -950,11 +956,13 @@ 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, 531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, + 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */, 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, + 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 67396556..19845f26 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -57,7 +57,7 @@ struct ContinueWatchingView: View { .foregroundColor(.primary) .lineLimit(1) if item.type == "Episode" { - Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")") + Text("• \(item.getEpisodeLocator()) - \(item.name ?? "")") .font(.callout) .fontWeight(.semibold) .foregroundColor(.secondary) diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 3ad6bd5e..9b1c817c 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -103,7 +103,7 @@ struct SeasonItemView: View { .opacity(1), alignment: .topTrailing).opacity(1) VStack(alignment: .leading) { HStack { - Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) + Text(episode.getEpisodeLocator()).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) diff --git a/JellyfinPlayer/VideoUpNextView.swift b/JellyfinPlayer/VideoUpNextView.swift index 79ab46ab..505e47fa 100644 --- a/JellyfinPlayer/VideoUpNextView.swift +++ b/JellyfinPlayer/VideoUpNextView.swift @@ -15,13 +15,6 @@ class UpNextViewModel: ObservableObject { @Published var item: BaseItemDto? = nil var delegate: PlayerViewController? - func getEpisodeLocator() -> String { - if let seasonNo = item?.parentIndexNumber, let episodeNo = item?.indexNumber { - return "S\(seasonNo):E\(episodeNo)" - } - return "" - } - func nextUp() { if delegate != nil { delegate?.setPlayerToNextUp() @@ -43,7 +36,7 @@ struct VideoUpNextView: View { .foregroundColor(.white) .font(.subheadline) .fontWeight(.semibold) - Text(viewModel.getEpisodeLocator()) + Text(viewModel.item.getEpisodeLocator()) .foregroundColor(.secondary) .font(.caption) } diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index 55a4f296..041913b0 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -73,6 +73,13 @@ extension BaseItemDto { let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" return URL(string: urlString)! } + + func getEpisodeLocator() -> String { + if let seasonNo = self.parentIndexNumber, let episodeNo = self.indexNumber { + return "S\(seasonNo):E\(episodeNo)" + } + return "" + } func getSeriesBackdropImage(maxWidth: Int) -> URL { let imageType = "Backdrop" @@ -104,6 +111,7 @@ extension BaseItemDto { let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + //print(urlString) return URL(string: urlString)! } diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index bcfc516d..6830dcbc 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -61,7 +61,9 @@ final class SessionManager { #else header.append("Client=\"SwiftFin iOS\", ") #endif + header.append("Device=\"\(deviceName)\", ") + #if os(tvOS) header.append("DeviceId=\"tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")\", ") deviceID = "tvOS_\(UIDevice.current.identifierForVendor!.uuidString)_\(user?.user_id ?? "")" diff --git a/Shared/ViewModels/DetailItemViewModel.swift b/Shared/ViewModels/DetailItemViewModel.swift index 2dba5c83..4d805224 100644 --- a/Shared/ViewModels/DetailItemViewModel.swift +++ b/Shared/ViewModels/DetailItemViewModel.swift @@ -12,19 +12,30 @@ import Foundation import JellyfinAPI class DetailItemViewModel: ViewModel { - @Published - var item: BaseItemDto - - @Published - var isWatched = false - @Published - var isFavorited = false + @Published var item: BaseItemDto + @Published var similarItems: [BaseItemDto] = [] + + @Published var isWatched = false + @Published var isFavorited = false init(item: BaseItemDto) { self.item = item isFavorited = item.userData?.isFavorite ?? false isWatched = item.userData?.played ?? false super.init() + + getRelatedItems() + } + + func getRelatedItems() { + LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestCompletion(completion: completion) + }, receiveValue: { [weak self] response in + self?.similarItems = response.items ?? [] + }) + .store(in: &cancellables) } func updateWatchState() { diff --git a/Shared/ViewModels/SeasonItemViewModel.swift b/Shared/ViewModels/SeasonItemViewModel.swift index c954a255..75efa20f 100644 --- a/Shared/ViewModels/SeasonItemViewModel.swift +++ b/Shared/ViewModels/SeasonItemViewModel.swift @@ -11,17 +11,13 @@ import Combine import Foundation import JellyfinAPI -final class SeasonItemViewModel: ViewModel { - @Published - var item: BaseItemDto +final class SeasonItemViewModel: DetailItemViewModel { + @Published var episodes = [BaseItemDto]() - @Published - var episodes = [BaseItemDto]() - - init(item: BaseItemDto) { + override init(item: BaseItemDto) { + super.init(item: item) self.item = item - super.init() - + requestEpisodes() } diff --git a/Shared/ViewModels/SeriesItemViewModel.swift b/Shared/ViewModels/SeriesItemViewModel.swift index b041f754..02f6df5b 100644 --- a/Shared/ViewModels/SeriesItemViewModel.swift +++ b/Shared/ViewModels/SeriesItemViewModel.swift @@ -11,18 +11,45 @@ import Combine import Foundation import JellyfinAPI -final class SeriesItemViewModel: ViewModel { - @Published - var item: BaseItemDto - - @Published - var seasons = [BaseItemDto]() - - init(item: BaseItemDto) { +final class SeriesItemViewModel: DetailItemViewModel { + @Published var seasons = [BaseItemDto]() + @Published var nextUpItem: BaseItemDto? + + override init(item: BaseItemDto) { + super.init(item: item) self.item = item - super.init() - + requestSeasons() + getNextUp() + } + + func getNextUp() { + TvShowsAPI.getNextUp(userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestCompletion(completion: completion) + }, receiveValue: { [weak self] response in + self?.nextUpItem = response.items?.first ?? nil + }) + .store(in: &cancellables) + } + + func getRunYears() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy" + + var startYear: String? = nil + var endYear: String? = nil + + if(item.premiereDate != nil) { + startYear = dateFormatter.string(from: item.premiereDate!) + } + + if(item.endDate != nil) { + endYear = dateFormatter.string(from: item.endDate!) + } + + return "\(startYear ?? "Unknown") - \(endYear ?? "Present")" } func requestSeasons() {