[create-pull-request] automated change

This commit is contained in:
acvigue 2021-06-25 18:46:43 +00:00 committed by GitHub
parent 75e7081cd6
commit e6ede1280d
28 changed files with 524 additions and 592 deletions

View File

@ -151,8 +151,7 @@ struct ConnectToServerView: View {
} }
.onAppear(perform: self.viewModel.discoverServers) .onAppear(perform: self.viewModel.discoverServers)
} }
} } else {
else {
ProgressView() ProgressView()
} }
} }

View File

@ -11,8 +11,7 @@ import SwiftUI
class AudioViewController: UIViewController { class AudioViewController: UIViewController {
var height : CGFloat = 420 var height: CGFloat = 420
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -21,8 +20,7 @@ class AudioViewController: UIViewController {
} }
func prepareAudioView(audioTracks: [AudioTrack], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) func prepareAudioView(audioTracks: [AudioTrack], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) {
{
let contentView = UIHostingController(rootView: AudioView(selectedTrack: selectedTrack, audioTrackArray: audioTracks, delegate: delegate)) let contentView = UIHostingController(rootView: AudioView(selectedTrack: selectedTrack, audioTrackArray: audioTracks, delegate: delegate))
self.view.addSubview(contentView.view) self.view.addSubview(contentView.view)
contentView.view.translatesAutoresizingMaskIntoConstraints = false contentView.view.translatesAutoresizingMaskIntoConstraints = false
@ -37,24 +35,23 @@ class AudioViewController: UIViewController {
struct AudioView: View { struct AudioView: View {
@State var selectedTrack : Int32 = -1 @State var selectedTrack: Int32 = -1
@State var audioTrackArray: [AudioTrack] = [] @State var audioTrackArray: [AudioTrack] = []
weak var delegate: VideoPlayerSettingsDelegate? weak var delegate: VideoPlayerSettingsDelegate?
var body : some View { var body : some View {
NavigationView { NavigationView {
VStack() { VStack {
List(audioTrackArray, id: \.id) { track in List(audioTrackArray, id: \.id) { track in
Button(action: { Button(action: {
delegate?.selectNew(audioTrack: track.id) delegate?.selectNew(audioTrack: track.id)
selectedTrack = track.id selectedTrack = track.id
}, label: { }, label: {
HStack(spacing: 10){ HStack(spacing: 10) {
if track.id == selectedTrack { if track.id == selectedTrack {
Image(systemName: "checkmark") Image(systemName: "checkmark")
} } else {
else {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.hidden() .hidden()
} }

View File

@ -12,13 +12,12 @@ import JellyfinAPI
class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate { class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate {
var videoPlayer : VideoPlayerViewController? = nil var videoPlayer: VideoPlayerViewController?
var subtitleViewController : SubtitlesViewController? = nil var subtitleViewController: SubtitlesViewController?
var audioViewController : AudioViewController? = nil var audioViewController: AudioViewController?
var mediaInfoController : MediaInfoViewController? = nil var mediaInfoController: MediaInfoViewController?
var infoContainerPos : CGRect? = nil var infoContainerPos: CGRect?
var tabBarHeight : CGFloat = 0 var tabBarHeight: CGFloat = 0
// override func viewWillAppear(_ animated: Bool) { // override func viewWillAppear(_ animated: Bool) {
// tabBar.standardAppearance.backgroundColor = .clear // tabBar.standardAppearance.backgroundColor = .clear
@ -52,7 +51,7 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate
} }
func setupInfoViews(mediaItem: BaseItemDto, subtitleTracks: [Subtitle], selectedSubtitleTrack : Int32, audioTracks: [AudioTrack], selectedAudioTrack: Int32, delegate: VideoPlayerSettingsDelegate) { func setupInfoViews(mediaItem: BaseItemDto, subtitleTracks: [Subtitle], selectedSubtitleTrack: Int32, audioTracks: [AudioTrack], selectedAudioTrack: Int32, delegate: VideoPlayerSettingsDelegate) {
mediaInfoController?.setMedia(item: mediaItem) mediaInfoController?.setMedia(item: mediaItem)
@ -65,8 +64,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate
} }
} }
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
@ -83,7 +80,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate
} }
} }
break break
case "Info": case "Info":
@ -97,7 +93,7 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate
} }
break break
case "Subtitles": case "Subtitles":
if var height = subtitleViewController?.height{ if var height = subtitleViewController?.height {
height += tabBarHeight height += tabBarHeight
UIView.animate(withDuration: 0.6, delay: 0, options: .curveEaseOut) { [self] in UIView.animate(withDuration: 0.6, delay: 0, options: .curveEaseOut) { [self] in
videoPlayer?.infoViewContainer.frame = CGRect(x: pos.minX, y: pos.minY, width: pos.width, height: height) videoPlayer?.infoViewContainer.frame = CGRect(x: pos.minX, y: pos.minY, width: pos.width, height: height)
@ -115,8 +111,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate
return true return true
} }
// MARK: - Navigation // MARK: - Navigation
// // In a storyboard-based application, you will often want to do a little preparation before navigation // // In a storyboard-based application, you will often want to do a little preparation before navigation

View File

@ -13,8 +13,7 @@ import JellyfinAPI
class MediaInfoViewController: UIViewController { class MediaInfoViewController: UIViewController {
private var contentView: UIHostingController<MediaInfoView>! private var contentView: UIHostingController<MediaInfoView>!
var height : CGFloat = 0 var height: CGFloat = 0
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -22,8 +21,7 @@ class MediaInfoViewController: UIViewController {
tabBarItem.title = "Info" tabBarItem.title = "Info"
} }
func setMedia(item: BaseItemDto) func setMedia(item: BaseItemDto) {
{
contentView = UIHostingController(rootView: MediaInfoView(item: item)) contentView = UIHostingController(rootView: MediaInfoView(item: item))
self.view.addSubview(contentView.view) self.view.addSubview(contentView.view)
contentView.view.translatesAutoresizingMaskIntoConstraints = false contentView.view.translatesAutoresizingMaskIntoConstraints = false
@ -38,7 +36,7 @@ class MediaInfoViewController: UIViewController {
} }
struct MediaInfoView: View { struct MediaInfoView: View {
@State var item : BaseItemDto? = nil @State var item: BaseItemDto?
var body: some View { var body: some View {
if let item = item { if let item = item {
@ -58,9 +56,7 @@ struct MediaInfoView: View {
Text(item.name ?? "Episode") Text(item.name ?? "Episode")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} } else {
else
{
Text(item.name ?? "Movie") Text(item.name ?? "Movie")
.fontWeight(.bold) .fontWeight(.bold)
} }
@ -102,7 +98,6 @@ struct MediaInfoView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
Spacer() Spacer()
} }
@ -111,15 +106,13 @@ struct MediaInfoView: View {
} }
.padding(.leading, 350) .padding(.leading, 350)
.padding(.trailing, 125) .padding(.trailing, 125)
} } else {
else {
EmptyView() EmptyView()
} }
} }
func formatDate(date: Date) -> String {
func formatDate(date : Date) -> String{
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "d MMM yyyy" formatter.dateFormat = "d MMM yyyy"

View File

@ -11,8 +11,7 @@ import SwiftUI
class SubtitlesViewController: UIViewController { class SubtitlesViewController: UIViewController {
var height : CGFloat = 420 var height: CGFloat = 420
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -21,8 +20,7 @@ class SubtitlesViewController: UIViewController {
} }
func prepareSubtitleView(subtitleTracks: [Subtitle], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) func prepareSubtitleView(subtitleTracks: [Subtitle], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) {
{
let contentView = UIHostingController(rootView: SubtitleView(selectedTrack: selectedTrack, subtitleTrackArray: subtitleTracks, delegate: delegate)) let contentView = UIHostingController(rootView: SubtitleView(selectedTrack: selectedTrack, subtitleTrackArray: subtitleTracks, delegate: delegate))
self.view.addSubview(contentView.view) self.view.addSubview(contentView.view)
contentView.view.translatesAutoresizingMaskIntoConstraints = false contentView.view.translatesAutoresizingMaskIntoConstraints = false
@ -36,25 +34,23 @@ class SubtitlesViewController: UIViewController {
struct SubtitleView: View { struct SubtitleView: View {
@State var selectedTrack : Int32 = -1 @State var selectedTrack: Int32 = -1
@State var subtitleTrackArray: [Subtitle] = [] @State var subtitleTrackArray: [Subtitle] = []
weak var delegate: VideoPlayerSettingsDelegate? weak var delegate: VideoPlayerSettingsDelegate?
var body : some View { var body : some View {
NavigationView { NavigationView {
VStack() { VStack {
List(subtitleTrackArray, id: \.id) { track in List(subtitleTrackArray, id: \.id) { track in
Button(action: { Button(action: {
delegate?.selectNew(subtitleTrack: track.id) delegate?.selectNew(subtitleTrack: track.id)
selectedTrack = track.id selectedTrack = track.id
}, label: { }, label: {
HStack(spacing: 10){ HStack(spacing: 10) {
if track.id == selectedTrack { if track.id == selectedTrack {
Image(systemName: "checkmark") Image(systemName: "checkmark")
} } else {
else {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.hidden() .hidden()
} }

View File

@ -19,8 +19,7 @@ protocol VideoPlayerSettingsDelegate: AnyObject {
func selectNew(subtitleTrack id: Int32) func selectNew(subtitleTrack id: Int32)
} }
class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, VLCMediaPlayerDelegate, VLCMediaDelegate, UIGestureRecognizerDelegate { class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, VLCMediaPlayerDelegate, VLCMediaDelegate, UIGestureRecognizerDelegate {
@IBOutlet weak var videoContentView: UIView! @IBOutlet weak var videoContentView: UIView!
@IBOutlet weak var controlsView: UIView! @IBOutlet weak var controlsView: UIView!
@ -37,12 +36,12 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
@IBOutlet weak var infoViewContainer: UIView! @IBOutlet weak var infoViewContainer: UIView!
var infoPanelDisplayPoint : CGPoint = .zero var infoPanelDisplayPoint: CGPoint = .zero
var infoPanelHiddenPoint : CGPoint = .zero var infoPanelHiddenPoint: CGPoint = .zero
var containerViewController: InfoTabBarViewController? var containerViewController: InfoTabBarViewController?
var focusedOnTabBar : Bool = false var focusedOnTabBar: Bool = false
var showingInfoPanel : Bool = false var showingInfoPanel: Bool = false
var mediaPlayer = VLCMediaPlayer() var mediaPlayer = VLCMediaPlayer()
@ -69,33 +68,28 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
var showingControls: Bool = false var showingControls: Bool = false
var loading: Bool = true var loading: Bool = true
var initialSeekPos : CGFloat = 0 var initialSeekPos: CGFloat = 0
var videoPos: Double = 0 var videoPos: Double = 0
var videoDuration: Double = 0 var videoDuration: Double = 0
var controlsAppearTime: Double = 0 var controlsAppearTime: Double = 0
var manifest: BaseItemDto = BaseItemDto() var manifest: BaseItemDto = BaseItemDto()
var playbackItem = PlaybackItem() var playbackItem = PlaybackItem()
var playSessionId: String = "" var playSessionId: String = ""
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocus(in: context, with: coordinator) super.didUpdateFocus(in: context, with: coordinator)
// Check if focused on the tab bar, allows for swipe up to dismiss the info panel // Check if focused on the tab bar, allows for swipe up to dismiss the info panel
if context.nextFocusedView!.description.contains("UITabBarButton") if context.nextFocusedView!.description.contains("UITabBarButton") {
{
// Set value after half a second so info panel is not dismissed instantly when swiping up from content // 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.5) {
self.focusedOnTabBar = true self.focusedOnTabBar = true
} }
} } else {
else
{
focusedOnTabBar = false focusedOnTabBar = false
} }
@ -115,7 +109,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
// Black gradient behind transport bar // Black gradient behind transport bar
let gradientLayer:CAGradientLayer = CAGradientLayer() let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.frame.size = self.gradientView.frame.size gradientLayer.frame.size = self.gradientView.frame.size
gradientLayer.colors = [UIColor.black.withAlphaComponent(0.6).cgColor, UIColor.black.withAlphaComponent(0).cgColor] gradientLayer.colors = [UIColor.black.withAlphaComponent(0.6).cgColor, UIColor.black.withAlphaComponent(0).cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0) gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
@ -179,7 +173,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
let item = PlaybackItem() let item = PlaybackItem()
let streamURL : URL let streamURL: URL
// Item is being transcoded by request of server // Item is being transcoded by request of server
if let transcodiungUrl = mediaSource.transcodingUrl { if let transcodiungUrl = mediaSource.transcodingUrl {
@ -187,8 +181,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")! streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")!
} }
// Item will be directly played by the client // Item will be directly played by the client
else else {
{
item.videoType = .directPlay item.videoType = .directPlay
streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")! streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")!
} }
@ -202,7 +195,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
for stream in mediaSource.mediaStreams! { for stream in mediaSource.mediaStreams! {
if stream.type == .subtitle { if stream.type == .subtitle {
var deliveryUrl: URL? = nil var deliveryUrl: URL?
if stream.deliveryMethod == .external { if stream.deliveryMethod == .external {
deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")!
@ -210,7 +203,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "")
if stream.isDefault == true{ if stream.isDefault == true {
selectedCaptionTrack = Int32(stream.index!) selectedCaptionTrack = Int32(stream.index!)
} }
@ -235,7 +228,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
selectedAudioTrack = audioTrackArray.first!.id selectedAudioTrack = audioTrackArray.first!.id
} }
self.sendPlayReport() self.sendPlayReport()
playbackItem = item playbackItem = item
@ -274,7 +266,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
} }
@ -363,7 +354,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) { if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) {
if let artworkImage = UIImage(data: imageData as Data) { if let artworkImage = UIImage(data: imageData as Data) {
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (size) -> UIImage in let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
return artworkImage return artworkImage
}) })
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
@ -372,11 +363,10 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
UIApplication.shared.beginReceivingRemoteControlEvents() UIApplication.shared.beginReceivingRemoteControlEvents()
} }
func updateNowPlayingCenter(time : Double?, playing : Bool?) { func updateNowPlayingCenter(time: Double?, playing: Bool?) {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]() var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
@ -391,8 +381,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
// Grabs a refference to the info panel view controller // Grabs a refference to the info panel view controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "infoView" { if segue.identifier == "infoView" {
@ -405,7 +393,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
// MARK: Player functions // MARK: Player functions
// Animate the scrubber when playing state changes // Animate the scrubber when playing state changes
func animateScrubber() { func animateScrubber() {
let y : CGFloat = playing ? 0 : -20 let y: CGFloat = playing ? 0 : -20
let height: CGFloat = playing ? 10 : 30 let height: CGFloat = playing ? 10 : 30
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: { UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: {
@ -413,7 +401,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
}) })
} }
func pause() { func pause() {
playing = false playing = false
mediaPlayer.pause() mediaPlayer.pause()
@ -424,7 +411,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
animateScrubber() 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) 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)
} }
func play () { func play () {
@ -438,7 +425,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
animateScrubber() animateScrubber()
} }
func toggleInfoContainer() { func toggleInfoContainer() {
showingInfoPanel.toggle() showingInfoPanel.toggle()
@ -456,7 +442,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) { [self] in UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) { [self] in
infoViewContainer.center = showingInfoPanel ? infoPanelDisplayPoint : infoPanelHiddenPoint infoViewContainer.center = showingInfoPanel ? infoPanelDisplayPoint : infoPanelHiddenPoint
} }
@ -467,23 +453,22 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
let playPauseGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped)) let playPauseGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped))
let playPauseType = UIPress.PressType.playPause let playPauseType = UIPress.PressType.playPause
playPauseGesture.allowedPressTypes = [NSNumber(value: playPauseType.rawValue)]; playPauseGesture.allowedPressTypes = [NSNumber(value: playPauseType.rawValue)]
view.addGestureRecognizer(playPauseGesture) view.addGestureRecognizer(playPauseGesture)
let selectGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped)) let selectGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped))
let selectType = UIPress.PressType.select let selectType = UIPress.PressType.select
selectGesture.allowedPressTypes = [NSNumber(value: selectType.rawValue)]; selectGesture.allowedPressTypes = [NSNumber(value: selectType.rawValue)]
view.addGestureRecognizer(selectGesture) view.addGestureRecognizer(selectGesture)
let backTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.backButtonPressed(tap:))) let backTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.backButtonPressed(tap:)))
let backPress = UIPress.PressType.menu let backPress = UIPress.PressType.menu
backTapGesture.allowedPressTypes = [NSNumber(value: backPress.rawValue)]; backTapGesture.allowedPressTypes = [NSNumber(value: backPress.rawValue)]
view.addGestureRecognizer(backTapGesture) view.addGestureRecognizer(backTapGesture)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.userPanned(panGestureRecognizer:))) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.userPanned(panGestureRecognizer:)))
view.addGestureRecognizer(panGestureRecognizer) view.addGestureRecognizer(panGestureRecognizer)
let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:))) let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:)))
swipeRecognizer.direction = .right swipeRecognizer.direction = .right
view.addGestureRecognizer(swipeRecognizer) view.addGestureRecognizer(swipeRecognizer)
@ -494,7 +479,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
@objc func backButtonPressed(tap : UITapGestureRecognizer) { @objc func backButtonPressed(tap: UITapGestureRecognizer) {
// Dismiss info panel // Dismiss info panel
if showingInfoPanel { if showingInfoPanel {
@ -505,16 +490,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
// Cancel seek and move back to initial position // Cancel seek and move back to initial position
if(seeking) { if seeking {
scrubLabel.isHidden = true scrubLabel.isHidden = true
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: {
self.scrubberView.frame = CGRect(x: self.initialSeekPos, y: 0, width: 2, height: 10) self.scrubberView.frame = CGRect(x: self.initialSeekPos, y: 0, width: 2, height: 10)
}) })
play() play()
seeking = false seeking = false
} } else {
else
{
// Dismiss view // Dismiss view
mediaPlayer.stop() mediaPlayer.stop()
sendStopReport() sendStopReport()
@ -522,7 +505,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
} }
@objc func userPanned(panGestureRecognizer : UIPanGestureRecognizer) { @objc func userPanned(panGestureRecognizer: UIPanGestureRecognizer) {
if loading { if loading {
return return
} }
@ -552,7 +535,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
// Save current position if seek is cancelled and show the scrubLabel // Save current position if seek is cancelled and show the scrubLabel
if(!seeking) { if !seeking {
initialSeekPos = self.scrubberView.frame.minX initialSeekPos = self.scrubberView.frame.minX
seeking = true seeking = true
self.scrubLabel.isHidden = false self.scrubLabel.isHidden = false
@ -569,7 +552,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
}) })
} }
// Not currently used // Not currently used
@ -606,9 +588,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
controlsView.isHidden = false controlsView.isHidden = false
controlsAppearTime = CACurrentMediaTime() controlsAppearTime = CACurrentMediaTime()
// Move to seeked position // Move to seeked position
if(seeking) { if seeking {
scrubLabel.isHidden = true scrubLabel.isHidden = true
// Move current time to the scrubbed position // Move current time to the scrubbed position
@ -634,7 +615,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
playing ? pause() : play() playing ? pause() : play()
} }
// MARK: Jellyfin Playstate updates // MARK: Jellyfin Playstate updates
func sendProgressReport(eventName: String) { func sendProgressReport(eventName: String) {
if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" {
@ -678,7 +658,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
.store(in: &cancellables) .store(in: &cancellables)
} }
// MARK: VLC Delegate // MARK: VLC Delegate
func mediaPlayerStateChanged(_ aNotification: Notification!) { func mediaPlayerStateChanged(_ aNotification: Notification!) {
@ -781,7 +760,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
} }
} }
// MARK: Settings Delegate // MARK: Settings Delegate
func selectNew(audioTrack id: Int32) { func selectNew(audioTrack id: Int32) {
selectedAudioTrack = id selectedAudioTrack = id
@ -797,7 +775,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
containerViewController?.setupInfoViews(mediaItem: manifest, subtitleTracks: subtitleTrackArray, selectedSubtitleTrack: selectedCaptionTrack, audioTracks: audioTrackArray, selectedAudioTrack: selectedAudioTrack, delegate: self) containerViewController?.setupInfoViews(mediaItem: manifest, subtitleTracks: subtitleTrackArray, selectedSubtitleTrack: selectedCaptionTrack, audioTracks: audioTrackArray, selectedAudioTrack: selectedAudioTrack, delegate: self)
} }
func formatSecondsToHMS(_ seconds: Double) -> String { func formatSecondsToHMS(_ seconds: Double) -> String {
let timeHMSFormatter: DateComponentsFormatter = { let timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()

View File

@ -56,7 +56,7 @@ struct ContinueWatchingView: View {
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary) .foregroundColor(.primary)
.lineLimit(1) .lineLimit(1)
if(item.type == "Episode") { if item.type == "Episode" {
Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")") Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")")
.font(.callout) .font(.callout)
.fontWeight(.semibold) .fontWeight(.semibold)

View File

@ -16,7 +16,7 @@ struct HomeView: View {
@ViewBuilder @ViewBuilder
var innerBody: some View { var innerBody: some View {
if(viewModel.isLoading) { if viewModel.isLoading {
ProgressView() ProgressView()
} else { } else {
ScrollView { ScrollView {

View File

@ -23,15 +23,14 @@ struct LatestMediaView: View {
.shadow(radius: 4) .shadow(radius: 4)
.overlay( .overlay(
ZStack { ZStack {
if(item.userData!.played ?? false) { if item.userData!.played ?? false {
Image(systemName: "circle.fill") Image(systemName: "circle.fill")
.foregroundColor(.white) .foregroundColor(.white)
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(.systemBlue)) .foregroundColor(Color(.systemBlue))
} }
}.padding(2) }.padding(2)
.opacity(1) .opacity(1), alignment: .topTrailing).opacity(1)
, alignment: .topTrailing).opacity(1)
Text(item.seriesName ?? item.name ?? "") Text(item.seriesName ?? item.name ?? "")
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)

View File

@ -13,12 +13,12 @@ struct LibraryListView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
LazyVStack() { LazyVStack {
NavigationLink(destination: LazyView { NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites") LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")
}) { }) {
ZStack() { ZStack {
HStack() { HStack {
Spacer() Spacer()
Text("Your Favorites") Text("Your Favorites")
.foregroundColor(.black) .foregroundColor(.black)
@ -38,8 +38,8 @@ struct LibraryListView: View {
NavigationLink(destination: LazyView { NavigationLink(destination: LazyView {
Text("WIP") Text("WIP")
}) { }) {
ZStack() { ZStack {
HStack() { HStack {
Spacer() Spacer()
Text("All Genres") Text("All Genres")
.foregroundColor(.black) .foregroundColor(.black)
@ -57,14 +57,14 @@ struct LibraryListView: View {
.padding(.bottom, 15) .padding(.bottom, 15)
ForEach(viewModel.libraries, id: \.id) { library in ForEach(viewModel.libraries, id: \.id) { library in
if(library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows") { if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
NavigationLink(destination: LazyView { NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "")
}) { }) {
ZStack() { ZStack {
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
.opacity(0.4) .opacity(0.4)
HStack() { HStack {
Spacer() Spacer()
Text(library.name ?? "") Text(library.name ?? "")
.foregroundColor(.white) .foregroundColor(.white)

View File

@ -24,7 +24,7 @@ struct LibrarySearchView: View {
Spacer().frame(height: 6) Spacer().frame(height: 6)
SearchBar(text: $searchQuery) SearchBar(text: $searchQuery)
ZStack { ZStack {
if(!viewModel.isLoading) { if !viewModel.isLoading {
ScrollView(.vertical) { ScrollView(.vertical) {
if !viewModel.items.isEmpty { if !viewModel.items.isEmpty {
Spacer().frame(height: 16) Spacer().frame(height: 16)
@ -37,15 +37,14 @@ struct LibrarySearchView: View {
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
ZStack { ZStack {
if(item.userData!.played ?? false) { if item.userData!.played ?? false {
Image(systemName: "circle.fill") Image(systemName: "circle.fill")
.foregroundColor(.white) .foregroundColor(.white)
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(.systemBlue)) .foregroundColor(Color(.systemBlue))
} }
}.padding(2) }.padding(2)
.opacity(1) .opacity(1), alignment: .topTrailing).opacity(1)
, alignment: .topTrailing).opacity(1)
Text(item.name ?? "") Text(item.name ?? "")
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)

View File

@ -41,15 +41,14 @@ struct LibraryView: View {
.cornerRadius(10) .cornerRadius(10)
.overlay( .overlay(
ZStack { ZStack {
if(item.userData!.played ?? false) { if item.userData!.played ?? false {
Image(systemName: "circle.fill") Image(systemName: "circle.fill")
.foregroundColor(.white) .foregroundColor(.white)
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(.systemBlue)) .foregroundColor(Color(.systemBlue))
} }
}.padding(2) }.padding(2)
.opacity(1) .opacity(1), alignment: .topTrailing).opacity(1)
, alignment: .topTrailing).opacity(1)
Text(item.name ?? "") Text(item.name ?? "")
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)

View File

@ -78,15 +78,14 @@ struct SeasonItemView: View {
) )
.overlay( .overlay(
ZStack { ZStack {
if(episode.userData!.played ?? false) { if episode.userData!.played ?? false {
Image(systemName: "circle.fill") Image(systemName: "circle.fill")
.foregroundColor(.white) .foregroundColor(.white)
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(.systemBlue)) .foregroundColor(Color(.systemBlue))
} }
}.padding(2) }.padding(2)
.opacity(1) .opacity(1), alignment: .topTrailing).opacity(1)
, alignment: .topTrailing).opacity(1)
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline)

View File

@ -48,7 +48,7 @@ struct SettingsView: View {
SearchablePicker(label: "Preferred subtitle language", SearchablePicker(label: "Preferred subtitle language",
options: viewModel.langs, options: viewModel.langs,
optionToString: { $0.name }, optionToString: { $0.name },
selected:Binding<TrackLanguage>( selected: Binding<TrackLanguage>(
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto },
set: {autoSelectSubtitlesLangcode = $0.isoCode} set: {autoSelectSubtitlesLangcode = $0.isoCode}
) )
@ -71,7 +71,6 @@ struct SettingsView: View {
let nc = NotificationCenter.default let nc = NotificationCenter.default
nc.post(name: Notification.Name("didSignOut"), object: nil) nc.post(name: Notification.Name("didSignOut"), object: nil)
SessionManager.current.logout() SessionManager.current.logout()
} label: { } label: {
Text("Log out").font(.callout) Text("Log out").font(.callout)

View File

@ -54,9 +54,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
var controlsAppearTime: Double = 0 var controlsAppearTime: Double = 0
var isSeeking: Bool = false var isSeeking: Bool = false
var playerDestination: PlayerDestination = .local; var playerDestination: PlayerDestination = .local
var discoveredCastDevices: [GCKDevice] = []; var discoveredCastDevices: [GCKDevice] = []
var selectedCastDevice: GCKDevice?; var selectedCastDevice: GCKDevice?
var jellyfinCastChannel: GCKGenericChannel? var jellyfinCastChannel: GCKGenericChannel?
var remotePositionTicks: Int = 0 var remotePositionTicks: Int = 0
private var castDiscoveryManager: GCKDiscoveryManager { private var castDiscoveryManager: GCKDiscoveryManager {
@ -65,7 +65,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
private var castSessionManager: GCKSessionManager { private var castSessionManager: GCKSessionManager {
return GCKCastContext.sharedInstance().sessionManager return GCKCastContext.sharedInstance().sessionManager
} }
var hasSentRemoteSeek: Bool = false; var hasSentRemoteSeek: Bool = false
var selectedAudioTrack: Int32 = -1 var selectedAudioTrack: Int32 = -1
var selectedCaptionTrack: Int32 = -1 var selectedCaptionTrack: Int32 = -1
@ -78,10 +78,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
var playbackItem = PlaybackItem() var playbackItem = PlaybackItem()
var remoteTimeUpdateTimer: Timer? var remoteTimeUpdateTimer: Timer?
// MARK: IBActions // MARK: IBActions
@IBAction func seekSliderStart(_ sender: Any) { @IBAction func seekSliderStart(_ sender: Any) {
if(playerDestination == .local) { if playerDestination == .local {
sendProgressReport(eventName: "pause") sendProgressReport(eventName: "pause")
mediaPlayer.pause() mediaPlayer.pause()
} else { } else {
@ -112,7 +111,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
let offset = secondsScrubbedTo - videoPosition let offset = secondsScrubbedTo - videoPosition
if(playerDestination == .local) { if playerDestination == .local {
if offset > 0 { if offset > 0 {
mediaPlayer.jumpForward(Int32(offset)) mediaPlayer.jumpForward(Int32(offset))
} else { } else {
@ -131,7 +130,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
sendStopReport() sendStopReport()
mediaPlayer.stop() mediaPlayer.stop()
if(castSessionManager.hasConnectedCastSession()) { if castSessionManager.hasConnectedCastSession() {
castSessionManager.endSessionAndStopCasting(true) castSessionManager.endSessionAndStopCasting(true)
} }
@ -139,13 +138,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
@IBAction func controlViewTapped(_ sender: Any) { @IBAction func controlViewTapped(_ sender: Any) {
if(playerDestination == .local) { if playerDestination == .local {
videoControlsView.isHidden = true videoControlsView.isHidden = true
} }
} }
@IBAction func contentViewTapped(_ sender: Any) { @IBAction func contentViewTapped(_ sender: Any) {
if(playerDestination == .local) { if playerDestination == .local {
videoControlsView.isHidden = false videoControlsView.isHidden = false
controlsAppearTime = CACurrentMediaTime() controlsAppearTime = CACurrentMediaTime()
} }
@ -153,7 +152,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
@IBAction func jumpBackTapped(_ sender: Any) { @IBAction func jumpBackTapped(_ sender: Any) {
if paused == false { if paused == false {
if(playerDestination == .local) { if playerDestination == .local {
mediaPlayer.jumpBackward(15) mediaPlayer.jumpBackward(15)
} else { } else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)-15]) self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)-15])
@ -163,7 +162,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
@IBAction func jumpForwardTapped(_ sender: Any) { @IBAction func jumpForwardTapped(_ sender: Any) {
if paused == false { if paused == false {
if(playerDestination == .local) { if playerDestination == .local {
mediaPlayer.jumpForward(30) mediaPlayer.jumpForward(30)
} else { } else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)+30]) self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)+30])
@ -174,7 +173,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
@IBOutlet weak var mainActionButton: UIButton! @IBOutlet weak var mainActionButton: UIButton!
@IBAction func mainActionButtonPressed(_ sender: Any) { @IBAction func mainActionButtonPressed(_ sender: Any) {
if paused { if paused {
if(playerDestination == .local) { if playerDestination == .local {
mediaPlayer.play() mediaPlayer.play()
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
paused = false paused = false
@ -184,7 +183,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
paused = false paused = false
} }
} else { } else {
if(playerDestination == .local) { if playerDestination == .local {
mediaPlayer.pause() mediaPlayer.pause()
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
paused = true paused = true
@ -211,9 +210,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
} }
//MARK: Cast methods // MARK: Cast methods
@IBAction func castButtonPressed(_ sender: Any) { @IBAction func castButtonPressed(_ sender: Any) {
if(selectedCastDevice == nil) { if selectedCastDevice == nil {
castDeviceVC = VideoPlayerCastDeviceSelectorView() castDeviceVC = VideoPlayerCastDeviceSelectorView()
castDeviceVC?.delegate = self castDeviceVC?.delegate = self
@ -228,7 +227,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
} else { } else {
castSessionManager.endSessionAndStopCasting(true) castSessionManager.endSessionAndStopCasting(true)
selectedCastDevice = nil; selectedCastDevice = nil
self.castButton.isEnabled = true self.castButton.isEnabled = true
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
playerDestination = .local playerDestination = .local
@ -237,24 +236,24 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
func castPopoverDismissed() { func castPopoverDismissed() {
castDeviceVC?.dismiss(animated: true, completion: nil) castDeviceVC?.dismiss(animated: true, completion: nil)
if(playerDestination == .local) { if playerDestination == .local {
self.mediaPlayer.play() self.mediaPlayer.play()
} }
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
} }
func castDeviceChanged() { func castDeviceChanged() {
if(selectedCastDevice != nil) { if selectedCastDevice != nil {
playerDestination = .remote playerDestination = .remote
castSessionManager.add(self) castSessionManager.add(self)
castSessionManager.startSession(with: selectedCastDevice!) castSessionManager.startSession(with: selectedCastDevice!)
} }
} }
//MARK: Cast End // MARK: Cast End
func settingsPopoverDismissed() { func settingsPopoverDismissed() {
optionsVC?.dismiss(animated: true, completion: nil) optionsVC?.dismiss(animated: true, completion: nil)
if(playerDestination == .local) { if playerDestination == .local {
self.mediaPlayer.play() self.mediaPlayer.play()
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
} }
@ -270,7 +269,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
// Add handler for Pause Command // Add handler for Pause Command
commandCenter.pauseCommand.addTarget { _ in commandCenter.pauseCommand.addTarget { _ in
if(self.playerDestination == .local) { if self.playerDestination == .local {
self.mediaPlayer.pause() self.mediaPlayer.pause()
self.sendProgressReport(eventName: "pause") self.sendProgressReport(eventName: "pause")
} else { } else {
@ -282,7 +281,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
// Add handler for Play command // Add handler for Play command
commandCenter.playCommand.addTarget { _ in commandCenter.playCommand.addTarget { _ in
if(self.playerDestination == .local) { if self.playerDestination == .local {
self.mediaPlayer.play() self.mediaPlayer.play()
self.sendProgressReport(eventName: "unpause") self.sendProgressReport(eventName: "unpause")
} else { } else {
@ -294,7 +293,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
// Add handler for FF command // Add handler for FF command
commandCenter.seekForwardCommand.addTarget { _ in commandCenter.seekForwardCommand.addTarget { _ in
if(self.playerDestination == .local) { if self.playerDestination == .local {
self.mediaPlayer.jumpForward(30) self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate") self.sendProgressReport(eventName: "timeupdate")
} else { } else {
@ -305,7 +304,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
// Add handler for RW command // Add handler for RW command
commandCenter.seekBackwardCommand.addTarget { _ in commandCenter.seekBackwardCommand.addTarget { _ in
if(self.playerDestination == .local) { if self.playerDestination == .local {
self.mediaPlayer.jumpBackward(15) self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate") self.sendProgressReport(eventName: "timeupdate")
} else { } else {
@ -324,7 +323,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
let videoPosition = Double(self.mediaPlayer.time.intValue) let videoPosition = Double(self.mediaPlayer.time.intValue)
let offset = targetSeconds - videoPosition let offset = targetSeconds - videoPosition
if(self.playerDestination == .local) { if self.playerDestination == .local {
if offset > 0 { if offset > 0 {
self.mediaPlayer.jumpForward(Int32(offset)/1000) self.mediaPlayer.jumpForward(Int32(offset)/1000)
} else { } else {
@ -357,7 +356,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")" titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0))\(manifest.name ?? "")"
} }
if(!UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat) { if !UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat {
let value = UIInterfaceOrientation.landscapeRight.rawValue let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation") UIDevice.current.setValue(value, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation() UIViewController.attemptRotationToDeviceOrientation()
@ -366,7 +365,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
func mediaHasStartedPlaying() { func mediaHasStartedPlaying() {
castButton.isHidden = true; castButton.isHidden = true
let discoveryCriteria = GCKDiscoveryCriteria(applicationID: "F007D354") let discoveryCriteria = GCKDiscoveryCriteria(applicationID: "F007D354")
let gckCastOptions = GCKCastOptions(discoveryCriteria: discoveryCriteria) let gckCastOptions = GCKCastOptions(discoveryCriteria: discoveryCriteria)
GCKCastContext.setSharedInstanceWith(gckCastOptions) GCKCastContext.setSharedInstanceWith(gckCastOptions)
@ -376,9 +375,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
func didUpdateDeviceList() { func didUpdateDeviceList() {
let totalDevices = castDiscoveryManager.deviceCount; let totalDevices = castDiscoveryManager.deviceCount
discoveredCastDevices = [] discoveredCastDevices = []
if(totalDevices > 0) { if totalDevices > 0 {
for i in 0...totalDevices-1 { for i in 0...totalDevices-1 {
let device = castDiscoveryManager.device(at: i) let device = castDiscoveryManager.device(at: i)
discoveredCastDevices.append(device) discoveredCastDevices.append(device)
@ -403,7 +402,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
} }
//MARK: viewDidAppear // MARK: viewDidAppear
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
overrideUserInterfaceStyle = .dark overrideUserInterfaceStyle = .dark
@ -572,8 +571,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
sendPlayReport() sendPlayReport()
// 1 second = 10,000,000 ticks // 1 second = 10,000,000 ticks
var startTicks: Int64 = 0; var startTicks: Int64 = 0
if(remotePositionTicks == 0) { if remotePositionTicks == 0 {
print("Using server-reported start time") print("Using server-reported start time")
startTicks = manifest.userData?.playbackPositionTicks ?? 0 startTicks = manifest.userData?.playbackPositionTicks ?? 0
} else { } else {
@ -582,7 +581,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
if startTicks != 0 { if startTicks != 0 {
let videoPosition = Double(mediaPlayer.time.intValue / 1000); let videoPosition = Double(mediaPlayer.time.intValue / 1000)
let secondsScrubbedTo = startTicks / 10_000_000 let secondsScrubbedTo = startTicks / 10_000_000
let offset = secondsScrubbedTo - Int64(videoPosition) let offset = secondsScrubbedTo - Int64(videoPosition)
print("Seeking to position: \(secondsScrubbedTo)") print("Seeking to position: \(secondsScrubbedTo)")
@ -593,7 +592,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
} }
if(fetchCaptions) { if fetchCaptions {
print("Fetching captions.") print("Fetching captions.")
// Pause and load captions into memory. // Pause and load captions into memory.
mediaPlayer.pause() mediaPlayer.pause()
@ -633,15 +632,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} }
} }
//MARK: - GCKGenericChannelDelegate // MARK: - GCKGenericChannelDelegate
extension PlayerViewController: GCKGenericChannelDelegate { extension PlayerViewController: GCKGenericChannelDelegate {
@objc func updateRemoteTime() { @objc func updateRemoteTime() {
castButton.setImage(UIImage(named: "CastConnected"), for: .normal) castButton.setImage(UIImage(named: "CastConnected"), for: .normal)
if(!paused) { if !paused {
remotePositionTicks = remotePositionTicks + 2_000_000; //add 0.2 secs every timer evt. remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt.
} }
if(isSeeking == false) { if isSeeking == false {
let remainingTime = (manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000 let remainingTime = (manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000
let hours = remainingTime / 3600 let hours = remainingTime / 3600
let minutes = (remainingTime % 3600) / 60 let minutes = (remainingTime % 3600) / 60
@ -663,19 +662,19 @@ extension PlayerViewController: GCKGenericChannelDelegate {
if let data = message.data(using: .utf8) { if let data = message.data(using: .utf8) {
if let json = try? JSON(data: data) { if let json = try? JSON(data: data) {
let messageType = json["type"].string ?? "" let messageType = json["type"].string ?? ""
if(messageType == "playbackprogress") { if messageType == "playbackprogress" {
dump(json) dump(json)
if(remotePositionTicks > 100) { if remotePositionTicks > 100 {
if(hasSentRemoteSeek == false) { if hasSentRemoteSeek == false {
hasSentRemoteSeek = true; hasSentRemoteSeek = true
sendJellyfinCommand(command: "Seek", options: [ sendJellyfinCommand(command: "Seek", options: [
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position) "position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position)
]) ])
} }
} }
paused = json["data"]["PlayState"]["IsPaused"].boolValue paused = json["data"]["PlayState"]["IsPaused"].boolValue
self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0; self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0
if(remoteTimeUpdateTimer == nil) { if remoteTimeUpdateTimer == nil {
remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), userInfo: nil, repeats: true) remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), userInfo: nil, repeats: true)
} }
} }
@ -701,9 +700,9 @@ extension PlayerViewController: GCKGenericChannelDelegate {
jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil) jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil)
if(command == "Seek") { if command == "Seek" {
remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000) remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000)
//Send playback report as Jellyfin Chromecast isn't smarter than a rock. // Send playback report as Jellyfin Chromecast isn't smarter than a rock.
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
@ -717,15 +716,15 @@ extension PlayerViewController: GCKGenericChannelDelegate {
} }
} }
//MARK: - GCKSessionManagerListener // MARK: - GCKSessionManagerListener
extension PlayerViewController: GCKSessionManagerListener { extension PlayerViewController: GCKSessionManagerListener {
func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) { func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) {
self.sendStopReport() self.sendStopReport()
mediaPlayer.stop() mediaPlayer.stop()
playerDestination = .remote playerDestination = .remote
videoContentView.isHidden = true; videoContentView.isHidden = true
videoControlsView.isHidden = false; videoControlsView.isHidden = false
castButton.setImage(UIImage(named: "CastConnected"), for: .normal) castButton.setImage(UIImage(named: "CastConnected"), for: .normal)
manager.currentCastSession?.start() manager.currentCastSession?.start()
@ -765,11 +764,10 @@ extension PlayerViewController: GCKSessionManagerListener {
dump(error) dump(error)
} }
func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) {
print("didEnd") print("didEnd")
playerDestination = .local; playerDestination = .local
videoContentView.isHidden = false; videoContentView.isHidden = false
remoteTimeUpdateTimer?.invalidate() remoteTimeUpdateTimer?.invalidate()
castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
startLocalPlaybackEngine(false) startLocalPlaybackEngine(false)
@ -777,15 +775,15 @@ extension PlayerViewController: GCKSessionManagerListener {
func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) { func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) {
print("didSuspend") print("didSuspend")
playerDestination = .local; playerDestination = .local
videoContentView.isHidden = false; videoContentView.isHidden = false
remoteTimeUpdateTimer?.invalidate() remoteTimeUpdateTimer?.invalidate()
castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
startLocalPlaybackEngine(false) startLocalPlaybackEngine(false)
} }
} }
//MARK: - VLCMediaPlayer Delegates // MARK: - VLCMediaPlayer Delegates
extension PlayerViewController: VLCMediaPlayerDelegate { extension PlayerViewController: VLCMediaPlayerDelegate {
func mediaPlayerStateChanged(_ aNotification: Notification!) { func mediaPlayerStateChanged(_ aNotification: Notification!) {
let currentState: VLCMediaPlayerState = mediaPlayer.state let currentState: VLCMediaPlayerState = mediaPlayer.state
@ -851,18 +849,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
} }
} }
// MARK: End VideoPlayerVC
//MARK: End VideoPlayerVC
struct VLCPlayerWithControls: UIViewControllerRepresentable { struct VLCPlayerWithControls: UIViewControllerRepresentable {
var item: BaseItemDto var item: BaseItemDto
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@ -909,7 +896,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
} }
} }
//MARK: - Play State Update Methods // MARK: - Play State Update Methods
extension PlayerViewController { extension PlayerViewController {
func sendProgressReport(eventName: String) { func sendProgressReport(eventName: String) {
if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" {

View File

@ -43,9 +43,9 @@ struct VideoPlayerCastDeviceSelector: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
Group { Group {
if(!delegate.discoveredCastDevices.isEmpty) { if !delegate.discoveredCastDevices.isEmpty {
List(delegate.discoveredCastDevices, id: \.deviceID) { device in List(delegate.discoveredCastDevices, id: \.deviceID) { device in
HStack() { HStack {
Text(device.friendlyName!) Text(device.friendlyName!)
.font(.subheadline) .font(.subheadline)
.fontWeight(.medium) .fontWeight(.medium)
@ -55,7 +55,7 @@ struct VideoPlayerCastDeviceSelector: View {
delegate?.castDeviceChanged() delegate?.castDeviceChanged()
delegate?.castPopoverDismissed() delegate?.castPopoverDismissed()
} label: { } label: {
HStack() { HStack {
Text("Connect") Text("Connect")
.font(.caption) .font(.caption)
.fontWeight(.medium) .fontWeight(.medium)
@ -91,4 +91,3 @@ struct VideoPlayerCastDeviceSelector: View {
}.offset(y: UIDevice.current.userInterfaceIdiom == .pad ? 14 : 0) }.offset(y: UIDevice.current.userInterfaceIdiom == .pad ? 14 : 0)
} }
} }

View File

@ -16,7 +16,6 @@ import Darwin
let INADDR_ANY = in_addr(s_addr: 0) let INADDR_ANY = in_addr(s_addr: 0)
let INADDR_BROADCAST = in_addr(s_addr: 0xffffffff) let INADDR_BROADCAST = in_addr(s_addr: 0xffffffff)
/// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket. /// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket.
open class UDPBroadcastConnection { open class UDPBroadcastConnection {
@ -58,11 +57,11 @@ open class UDPBroadcastConnection {
/// - Throws: Throws a `ConnectionError` if an error occurs. /// - Throws: Throws a `ConnectionError` if an error occurs.
public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws { public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws {
self.address = sockaddr_in( self.address = sockaddr_in(
sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size), sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size),
sin_family: sa_family_t(AF_INET), sin_family: sa_family_t(AF_INET),
sin_port: UDPBroadcastConnection.htonsPort(port: port), sin_port: UDPBroadcastConnection.htonsPort(port: port),
sin_addr: INADDR_BROADCAST, sin_addr: INADDR_BROADCAST,
sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 ) sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 )
) )
self.handler = handler self.handler = handler
@ -81,7 +80,6 @@ open class UDPBroadcastConnection {
// MARK: Interface // MARK: Interface
/// Create a UDP socket for broadcasting and set up cancel and event handlers /// Create a UDP socket for broadcasting and set up cancel and event handlers
/// ///
/// - Throws: Throws a `ConnectionError` if an error occurs. /// - Throws: Throws a `ConnectionError` if an error occurs.
@ -92,8 +90,8 @@ open class UDPBroadcastConnection {
guard newSocket > 0 else { throw ConnectionError.createSocketFailed } guard newSocket > 0 else { throw ConnectionError.createSocketFailed }
// Enable broadcast on socket // Enable broadcast on socket
var broadcastEnable = Int32(1); var broadcastEnable = Int32(1)
let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout<UInt32>.size)); let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout<UInt32>.size))
if ret == -1 { if ret == -1 {
debugPrint("Couldn't enable broadcast on socket") debugPrint("Couldn't enable broadcast on socket")
close(newSocket) close(newSocket)
@ -123,7 +121,7 @@ open class UDPBroadcastConnection {
// Set up cancel handler // Set up cancel handler
newResponseSource.setCancelHandler { newResponseSource.setCancelHandler {
//debugPrint("Closing UDP socket") // debugPrint("Closing UDP socket")
let UDPSocket = Int32(newResponseSource.handle) let UDPSocket = Int32(newResponseSource.handle)
shutdown(UDPSocket, SHUT_RDWR) shutdown(UDPSocket, SHUT_RDWR)
close(UDPSocket) close(UDPSocket)
@ -158,12 +156,12 @@ open class UDPBroadcastConnection {
guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1)) }) guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1)) })
else { else {
//debugPrint("Failed to get the address and port from the socket address received from recvfrom") // debugPrint("Failed to get the address and port from the socket address received from recvfrom")
self.closeConnection() self.closeConnection()
return return
} }
//debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") // debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)")
let responseBytes = Data(response[0..<bytesRead]) let responseBytes = Data(response[0..<bytesRead])
@ -213,7 +211,7 @@ open class UDPBroadcastConnection {
guard sent > 0 else { guard sent > 0 else {
if let errorString = String(validatingUTF8: strerror(errno)) { if let errorString = String(validatingUTF8: strerror(errno)) {
//debugPrint("UDP connection failed to send data: \(errorString)") // debugPrint("UDP connection failed to send data: \(errorString)")
} }
closeConnection() closeConnection()
throw ConnectionError.sendingMessageFailed(code: errno) throw ConnectionError.sendingMessageFailed(code: errno)
@ -221,7 +219,7 @@ open class UDPBroadcastConnection {
if sent == broadcastMessageLength { if sent == broadcastMessageLength {
// Success // Success
//debugPrint("UDP connection sent \(broadcastMessageLength) bytes") // debugPrint("UDP connection sent \(broadcastMessageLength) bytes")
} }
} }
} }
@ -276,15 +274,14 @@ open class UDPBroadcastConnection {
} }
} }
// MARK: - Private // MARK: - Private
/// Prevents crashes when blocking calls are pending and the app is paused (via Home button). /// Prevents crashes when blocking calls are pending and the app is paused (via Home button).
/// ///
/// - Parameter socket: The socket for which the signal should be disabled. /// - Parameter socket: The socket for which the signal should be disabled.
fileprivate func setNoSigPipe(socket: CInt) { fileprivate func setNoSigPipe(socket: CInt) {
var no_sig_pipe: Int32 = 1; var no_sig_pipe: Int32 = 1
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout<Int32>.size)); setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout<Int32>.size))
} }
fileprivate class func htonsPort(port: in_port_t) -> in_port_t { fileprivate class func htonsPort(port: in_port_t) -> in_port_t {
@ -298,8 +295,6 @@ open class UDPBroadcastConnection {
} }
// Created by Gunter Hager on 25.03.19. // Created by Gunter Hager on 25.03.19.
// Copyright © 2019 Gunter Hager. All rights reserved. // Copyright © 2019 Gunter Hager. All rights reserved.
// //

View File

@ -75,7 +75,7 @@ final class ConnectToServerViewModel: ViewModel {
.store(in: &cancellables) .store(in: &cancellables)
} }
func connectToServer(at url : URL) { func connectToServer(at url: URL) {
ServerEnvironment.current.create(with: url.absoluteString) ServerEnvironment.current.create(with: url.absoluteString)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { result in .sink(receiveCompletion: { result in

View File

@ -29,7 +29,7 @@ struct NextUpWidgetProvider: TimelineProvider {
let savedUser = SessionManager.current.user let savedUser = SessionManager.current.user
var tempCancellables = Set<AnyCancellable>() var tempCancellables = Set<AnyCancellable>()
if(server != nil && savedUser != nil) { if server != nil && savedUser != nil {
JellyfinAPI.basePath = server!.baseURI ?? "" JellyfinAPI.basePath = server!.baseURI ?? ""
TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
@ -74,7 +74,7 @@ struct NextUpWidgetProvider: TimelineProvider {
var tempCancellables = Set<AnyCancellable>() var tempCancellables = Set<AnyCancellable>()
if(server != nil && savedUser != nil) { if server != nil && savedUser != nil {
JellyfinAPI.basePath = server!.baseURI ?? "" JellyfinAPI.basePath = server!.baseURI ?? ""
TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],