Merge branch 'jellyfin:main' into nextUp
This commit is contained in:
commit
d1d44898e8
|
@ -151,8 +151,7 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
.onAppear(perform: self.viewModel.discoverServers)
|
.onAppear(perform: self.viewModel.discoverServers)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,14 @@ import TVVLCKit
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import Combine
|
import Combine
|
||||||
|
import Defaults
|
||||||
|
|
||||||
protocol VideoPlayerSettingsDelegate: AnyObject {
|
protocol VideoPlayerSettingsDelegate: AnyObject {
|
||||||
func selectNew(audioTrack id: Int32)
|
func selectNew(audioTrack id: Int32)
|
||||||
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!
|
||||||
|
@ -36,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()
|
||||||
|
|
||||||
|
@ -68,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,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)
|
||||||
|
@ -149,8 +144,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
|
|
||||||
func fetchVideo() {
|
func fetchVideo() {
|
||||||
// Fetch max bitrate from UserDefaults depending on current connection mode
|
// Fetch max bitrate from UserDefaults depending on current connection mode
|
||||||
let defaults = UserDefaults.standard
|
let maxBitrate = Defaults[.inNetworkBandwidth]
|
||||||
let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth")
|
|
||||||
|
|
||||||
// Build a device profile
|
// Build a device profile
|
||||||
let builder = DeviceProfileBuilder()
|
let builder = DeviceProfileBuilder()
|
||||||
|
@ -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,30 +181,29 @@ 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!)")!
|
||||||
}
|
}
|
||||||
|
|
||||||
item.videoUrl = streamURL
|
item.videoUrl = streamURL
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "")
|
let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "")
|
||||||
subtitleTrackArray.append(disableSubtitleTrack)
|
subtitleTrackArray.append(disableSubtitleTrack)
|
||||||
|
|
||||||
// Loop through media streams and add to array
|
// Loop through media streams and add to array
|
||||||
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!)")!
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt")
|
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!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,7 +213,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.type == .audio {
|
if stream.type == .audio {
|
||||||
let track = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
let track = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!))
|
||||||
|
|
||||||
if stream.isDefault! == true {
|
if stream.isDefault! == true {
|
||||||
selectedAudioTrack = Int32(stream.index!)
|
selectedAudioTrack = 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()
|
||||||
|
|
|
@ -108,6 +108,7 @@
|
||||||
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
||||||
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
||||||
|
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; };
|
||||||
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; };
|
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; };
|
||||||
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; };
|
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; };
|
||||||
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; };
|
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; };
|
||||||
|
@ -133,6 +134,10 @@
|
||||||
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
||||||
628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; };
|
628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; };
|
||||||
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||||
|
62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; };
|
||||||
|
62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; };
|
||||||
|
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; };
|
||||||
|
62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; };
|
||||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; };
|
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; };
|
||||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
||||||
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
||||||
|
@ -285,6 +290,7 @@
|
||||||
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
|
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
|
||||||
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||||
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
|
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
|
||||||
|
624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; };
|
||||||
625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
||||||
625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = "<group>"; };
|
625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = "<group>"; };
|
||||||
625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
|
625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -304,6 +310,7 @@
|
||||||
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
|
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
|
||||||
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||||
|
62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsExtension.swift; sourceTree = "<group>"; };
|
||||||
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
|
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
|
||||||
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
||||||
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
|
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -331,6 +338,7 @@
|
||||||
53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
||||||
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */,
|
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */,
|
||||||
535870912669D7A800D05A09 /* Introspect in Frameworks */,
|
535870912669D7A800D05A09 /* Introspect in Frameworks */,
|
||||||
|
62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */,
|
||||||
5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */,
|
5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */,
|
||||||
536D3D84267BEA550004248C /* ParallaxView in Frameworks */,
|
536D3D84267BEA550004248C /* ParallaxView in Frameworks */,
|
||||||
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
||||||
|
@ -343,6 +351,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */,
|
||||||
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
||||||
53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */,
|
53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */,
|
||||||
53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */,
|
53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */,
|
||||||
|
@ -591,6 +600,8 @@
|
||||||
6267B3D526710B8900A7371D /* CollectionExtensions.swift */,
|
6267B3D526710B8900A7371D /* CollectionExtensions.swift */,
|
||||||
6267B3D92671138200A7371D /* ImageExtensions.swift */,
|
6267B3D92671138200A7371D /* ImageExtensions.swift */,
|
||||||
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
|
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
|
||||||
|
62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */,
|
||||||
|
624C21742685CF60007F1390 /* SearchablePickerView.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -662,6 +673,7 @@
|
||||||
53A431BE266B0FFE0016769F /* JellyfinAPI */,
|
53A431BE266B0FFE0016769F /* JellyfinAPI */,
|
||||||
53ABFDEC26799D7700886593 /* ActivityIndicator */,
|
53ABFDEC26799D7700886593 /* ActivityIndicator */,
|
||||||
536D3D83267BEA550004248C /* ParallaxView */,
|
536D3D83267BEA550004248C /* ParallaxView */,
|
||||||
|
62CB3F472685BB3B003D0A6F /* Defaults */,
|
||||||
);
|
);
|
||||||
productName = "JellyfinPlayer tvOS";
|
productName = "JellyfinPlayer tvOS";
|
||||||
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
|
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
|
||||||
|
@ -693,6 +705,7 @@
|
||||||
53A431BC266B0FF20016769F /* JellyfinAPI */,
|
53A431BC266B0FF20016769F /* JellyfinAPI */,
|
||||||
625CB5792678C4A400530A6E /* ActivityIndicator */,
|
625CB5792678C4A400530A6E /* ActivityIndicator */,
|
||||||
53EC6E24267EB10F006DD26A /* SwiftyJSON */,
|
53EC6E24267EB10F006DD26A /* SwiftyJSON */,
|
||||||
|
62CB3F452685BAF7003D0A6F /* Defaults */,
|
||||||
);
|
);
|
||||||
productName = JellyfinPlayer;
|
productName = JellyfinPlayer;
|
||||||
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
||||||
|
@ -761,6 +774,7 @@
|
||||||
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */,
|
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */,
|
||||||
536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */,
|
536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */,
|
||||||
53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||||
|
62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -944,6 +958,7 @@
|
||||||
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */,
|
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */,
|
||||||
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
|
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
|
||||||
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
|
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
|
||||||
|
62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
||||||
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||||
5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */,
|
5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */,
|
||||||
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
|
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
|
||||||
|
@ -982,6 +997,7 @@
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
|
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
||||||
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */,
|
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */,
|
||||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
||||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||||
|
@ -1008,6 +1024,7 @@
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||||
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
||||||
|
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
|
||||||
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||||
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||||
|
@ -1066,7 +1083,7 @@
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 53;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 9R8RREG67J;
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
@ -1094,7 +1111,7 @@
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 53;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 9R8RREG67J;
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
@ -1243,7 +1260,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements;
|
CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 53;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 9R8RREG67J;
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
@ -1277,7 +1294,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements;
|
CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 53;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 9R8RREG67J;
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
|
@ -1309,7 +1326,7 @@
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 53;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_TEAM = 9R8RREG67J;
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -1334,7 +1351,7 @@
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 53;
|
CURRENT_PROJECT_VERSION = 54;
|
||||||
DEVELOPMENT_TEAM = 9R8RREG67J;
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
INFOPLIST_FILE = WidgetExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -1438,8 +1455,8 @@
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/kean/NukeUI";
|
repositoryURL = "https://github.com/kean/NukeUI";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = upToNextMajorVersion;
|
||||||
version = 0.3.0;
|
minimumVersion = 0.3.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = {
|
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = {
|
||||||
|
@ -1450,6 +1467,14 @@
|
||||||
minimumVersion = 1.1.0;
|
minimumVersion = 1.1.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/sindresorhus/Defaults";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 5.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -1533,6 +1558,16 @@
|
||||||
package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||||
productName = KeychainSwift;
|
productName = KeychainSwift;
|
||||||
};
|
};
|
||||||
|
62CB3F452685BAF7003D0A6F /* Defaults */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||||
|
productName = Defaults;
|
||||||
|
};
|
||||||
|
62CB3F472685BB3B003D0A6F /* Defaults */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||||
|
productName = Defaults;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
|
|
|
@ -19,6 +19,15 @@
|
||||||
"version": "0.6.0"
|
"version": "0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "Defaults",
|
||||||
|
"repositoryURL": "https://github.com/sindresorhus/Defaults",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "63d93f97ad545c8bceb125a8a36175ea705f7cf5",
|
||||||
|
"version": "5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "Gifu",
|
"package": "Gifu",
|
||||||
"repositoryURL": "https://github.com/kaishin/Gifu",
|
"repositoryURL": "https://github.com/kaishin/Gifu",
|
||||||
|
@ -60,8 +69,8 @@
|
||||||
"repositoryURL": "https://github.com/kean/NukeUI",
|
"repositoryURL": "https://github.com/kean/NukeUI",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "d2580b8d22b29c6244418d8e4b568f3162191460",
|
"revision": "4516371912149ac024dec361827931b46a69c217",
|
||||||
"version": "0.3.0"
|
"version": "0.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -105,7 +105,7 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Section(header: Text("Server Information")) {
|
Section(header: Text("Manual Connection")) {
|
||||||
TextField("Jellyfin Server URL", text: $uri)
|
TextField("Jellyfin Server URL", text: $uri)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
@ -123,23 +123,20 @@ struct ConnectToServerView: View {
|
||||||
.disabled(viewModel.isLoading || uri.isEmpty)
|
.disabled(viewModel.isLoading || uri.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Local Servers")) {
|
Section(header: Text("Discovered Servers")) {
|
||||||
if self.viewModel.searching {
|
if self.viewModel.searching {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
ForEach(self.viewModel.servers, id: \.id) { server in
|
ForEach(self.viewModel.servers, id: \.id) { server in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
print(server.url)
|
|
||||||
viewModel.connectToServer(at: server.url)
|
viewModel.connectToServer(at: server.url)
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
VStack {
|
|
||||||
Text(server.name)
|
Text(server.name)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text(server.host)
|
Text("• \(server.host)")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
|
|
@ -43,19 +43,6 @@ struct ContinueWatchingView: View {
|
||||||
.frame(width: 320, height: 180)
|
.frame(width: 320, height: 180)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 4)
|
.shadow(radius: 4)
|
||||||
.overlay(
|
|
||||||
Group {
|
|
||||||
if item.type == "Episode" {
|
|
||||||
Text("\(item.name ?? "")")
|
|
||||||
.font(.caption)
|
|
||||||
.padding(6)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
|
||||||
}.background(Color.black)
|
|
||||||
.opacity(0.8)
|
|
||||||
.cornerRadius(10.0)
|
|
||||||
.padding(6), alignment: .topTrailing
|
|
||||||
)
|
|
||||||
.overlay(
|
.overlay(
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||||
|
@ -63,12 +50,22 @@ struct ContinueWatchingView: View {
|
||||||
.frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7)
|
.frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7)
|
||||||
.padding(0), alignment: .bottomLeading
|
.padding(0), alignment: .bottomLeading
|
||||||
)
|
)
|
||||||
Text(item.seriesName ?? item.name ?? "")
|
HStack {
|
||||||
.font(.callout)
|
Text("\(item.seriesName ?? item.name ?? "")")
|
||||||
.fontWeight(.semibold)
|
.font(.callout)
|
||||||
.foregroundColor(.primary)
|
.fontWeight(.semibold)
|
||||||
.lineLimit(1)
|
.foregroundColor(.primary)
|
||||||
.frame(width: 320, alignment: .leading)
|
.lineLimit(1)
|
||||||
|
if item.type == "Episode" {
|
||||||
|
Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")")
|
||||||
|
.font(.callout)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.offset(x: -1.4)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}.frame(width: 320, alignment: .leading)
|
||||||
}.padding(.top, 10)
|
}.padding(.top, 10)
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 5)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,11 @@ 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 {
|
||||||
LazyVStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if !viewModel.resumeItems.isEmpty {
|
if !viewModel.resumeItems.isEmpty {
|
||||||
ContinueWatchingView(items: viewModel.resumeItems)
|
ContinueWatchingView(items: viewModel.resumeItems)
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,6 @@ struct HomeView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
innerBody
|
innerBody
|
||||||
.navigationTitle(MainTabView.Tab.home.localized)
|
.navigationTitle(MainTabView.Tab.home.localized)
|
||||||
/*
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
|
@ -70,6 +69,5 @@ struct HomeView: View {
|
||||||
.fullScreenCover(isPresented: $showingSettings) {
|
.fullScreenCover(isPresented: $showingSettings) {
|
||||||
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
|
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,16 @@ struct LatestMediaView: View {
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 4)
|
.shadow(radius: 4)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
|
if item.userData!.played ?? false {
|
||||||
|
Image(systemName: "circle.fill")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(Color(.systemBlue))
|
||||||
|
}
|
||||||
|
}.padding(2)
|
||||||
|
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||||
Text(item.seriesName ?? item.name ?? "")
|
Text(item.seriesName ?? item.name ?? "")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
|
@ -21,45 +21,54 @@ struct LibraryFilterView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ZStack {
|
VStack {
|
||||||
Form {
|
if viewModel.isLoading {
|
||||||
if viewModel.enabledFilterType.contains(.genre) {
|
ProgressView()
|
||||||
MultiSelector(label: "Genres",
|
} else {
|
||||||
options: viewModel.possibleGenres,
|
Form {
|
||||||
optionToString: { $0.name ?? "" },
|
if viewModel.enabledFilterType.contains(.genre) {
|
||||||
selected: $viewModel.modifiedFilters.withGenres)
|
MultiSelector(label: "Genres",
|
||||||
}
|
options: viewModel.possibleGenres,
|
||||||
if viewModel.enabledFilterType.contains(.filter) {
|
optionToString: { $0.name ?? "" },
|
||||||
MultiSelector(label: "Filters",
|
selected: $viewModel.modifiedFilters.withGenres)
|
||||||
options: viewModel.possibleItemFilters,
|
}
|
||||||
optionToString: { $0.localized },
|
if viewModel.enabledFilterType.contains(.filter) {
|
||||||
selected: $viewModel.modifiedFilters.filters)
|
MultiSelector(label: "Filters",
|
||||||
}
|
options: viewModel.possibleItemFilters,
|
||||||
if viewModel.enabledFilterType.contains(.tag) {
|
optionToString: { $0.localized },
|
||||||
MultiSelector(label: "Tags",
|
selected: $viewModel.modifiedFilters.filters)
|
||||||
options: viewModel.possibleTags,
|
}
|
||||||
optionToString: { $0 },
|
if viewModel.enabledFilterType.contains(.tag) {
|
||||||
selected: $viewModel.modifiedFilters.tags)
|
MultiSelector(label: "Tags",
|
||||||
}
|
options: viewModel.possibleTags,
|
||||||
if viewModel.enabledFilterType.contains(.sortBy) {
|
optionToString: { $0 },
|
||||||
MultiSelector(label: "Sort by",
|
selected: $viewModel.modifiedFilters.tags)
|
||||||
options: viewModel.possibleSortBys,
|
}
|
||||||
optionToString: { $0.localized },
|
if viewModel.enabledFilterType.contains(.sortBy) {
|
||||||
selected: $viewModel.modifiedFilters.sortBy)
|
Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) {
|
||||||
}
|
ForEach(viewModel.possibleSortBys, id: \.self) { so in
|
||||||
if viewModel.enabledFilterType.contains(.sortOrder) {
|
Text(so.localized).tag(so)
|
||||||
Picker(selection: $viewModel.modifiedFilters.sortOrder, label: Text("Order")) {
|
}
|
||||||
ForEach(viewModel.possibleSortOrders, id: \.self) { so in
|
}
|
||||||
Text("\(so.rawValue)").tag(so.rawValue)
|
}
|
||||||
|
if viewModel.enabledFilterType.contains(.sortOrder) {
|
||||||
|
Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) {
|
||||||
|
ForEach(viewModel.possibleSortOrders, id: \.self) { so in
|
||||||
|
Text(so.rawValue).tag(so)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Button {
|
||||||
if viewModel.isLoading {
|
viewModel.resetFilters()
|
||||||
ProgressView()
|
self.filters = viewModel.modifiedFilters
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
} label: {
|
||||||
|
Text("Reset")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Filters", displayMode: .inline)
|
.navigationBarTitle("Filter Results", displayMode: .inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||||
Button {
|
Button {
|
||||||
|
@ -70,6 +79,7 @@ struct LibraryFilterView: View {
|
||||||
}
|
}
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
|
viewModel.updateModifiedFilter()
|
||||||
self.filters = viewModel.modifiedFilters
|
self.filters = viewModel.modifiedFilters
|
||||||
presentationMode.wrappedValue.dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -74,6 +74,7 @@ struct LibraryListView: View {
|
||||||
}.padding(32)
|
}.padding(32)
|
||||||
}.background(Color.black)
|
}.background(Color.black)
|
||||||
.frame(minWidth: 100, maxWidth: .infinity)
|
.frame(minWidth: 100, maxWidth: .infinity)
|
||||||
|
.frame(height: 72)
|
||||||
}
|
}
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 5)
|
.shadow(radius: 5)
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -35,6 +35,16 @@ struct LibrarySearchView: View {
|
||||||
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
|
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
|
if item.userData!.played ?? false {
|
||||||
|
Image(systemName: "circle.fill")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(Color(.systemBlue))
|
||||||
|
}
|
||||||
|
}.padding(2)
|
||||||
|
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||||
Text(item.name ?? "")
|
Text(item.name ?? "")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
|
@ -13,6 +13,7 @@ struct LibraryView: View {
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
// MARK: tracks for grid
|
// MARK: tracks for grid
|
||||||
|
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||||
|
|
||||||
@State var isShowingSearchView = false
|
@State var isShowingSearchView = false
|
||||||
@State var isShowingFilterView = false
|
@State var isShowingFilterView = false
|
||||||
|
@ -38,6 +39,16 @@ struct LibraryView: View {
|
||||||
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
|
ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash())
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
|
if item.userData!.played ?? false {
|
||||||
|
Image(systemName: "circle.fill")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(Color(.systemBlue))
|
||||||
|
}
|
||||||
|
}.padding(2)
|
||||||
|
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||||
Text(item.name ?? "")
|
Text(item.name ?? "")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
@ -107,14 +118,14 @@ struct LibraryView: View {
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button(action: {
|
Label("Icon One", systemImage: "line.horizontal.3.decrease.circle")
|
||||||
|
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
|
||||||
|
.onTapGesture {
|
||||||
isShowingFilterView = true
|
isShowingFilterView = true
|
||||||
}) {
|
|
||||||
Image(systemName: "line.horizontal.3.decrease.circle")
|
|
||||||
}
|
}
|
||||||
Button(action: {
|
Button {
|
||||||
isShowingSearchView = true
|
isShowingSearchView = true
|
||||||
}) {
|
} label: {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,16 @@ struct SeasonItemView: View {
|
||||||
.frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7)
|
.frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7)
|
||||||
.padding(0), alignment: .bottomLeading
|
.padding(0), alignment: .bottomLeading
|
||||||
)
|
)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
|
if episode.userData!.played ?? false {
|
||||||
|
Image(systemName: "circle.fill")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(Color(.systemBlue))
|
||||||
|
}
|
||||||
|
}.padding(2)
|
||||||
|
.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)
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Defaults
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@ -14,19 +15,15 @@ struct SettingsView: View {
|
||||||
@ObservedObject var viewModel: SettingsViewModel
|
@ObservedObject var viewModel: SettingsViewModel
|
||||||
|
|
||||||
@Binding var close: Bool
|
@Binding var close: Bool
|
||||||
@State private var inNetworkStreamBitrate: Int = 40_000_000
|
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
|
||||||
@State private var outOfNetworkStreamBitrate: Int = 40_000_000
|
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate
|
||||||
@State private var autoSelectSubtitles: Bool = false
|
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
|
||||||
@State private var autoSelectSubtitlesLangcode: String = "none"
|
@Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode
|
||||||
|
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode
|
||||||
@State private var username: String = ""
|
@State private var username: String = ""
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
username = SessionManager.current.user.username ?? ""
|
username = SessionManager.current.user.username ?? ""
|
||||||
inNetworkStreamBitrate = defaults.integer(forKey: "InNetworkBandwidth")
|
|
||||||
outOfNetworkStreamBitrate = defaults.integer(forKey: "OutOfNetworkBandwidth")
|
|
||||||
autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles")
|
|
||||||
autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -37,27 +34,33 @@ struct SettingsView: View {
|
||||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||||
Text(bitrate.name).tag(bitrate.value)
|
Text(bitrate.name).tag(bitrate.value)
|
||||||
}
|
}
|
||||||
}.onChange(of: inNetworkStreamBitrate) { _ in
|
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
||||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||||
Text(bitrate.name).tag(bitrate.value)
|
Text(bitrate.name).tag(bitrate.value)
|
||||||
}
|
}
|
||||||
}.onChange(of: outOfNetworkStreamBitrate) { _ in
|
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Accessibility")) {
|
Section(header: Text("Accessibility")) {
|
||||||
Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in
|
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
||||||
let defaults = UserDefaults.standard
|
SearchablePicker(label: "Preferred subtitle language",
|
||||||
defaults.setValue(autoSelectSubtitles, forKey: "AutoSelectSubtitles")
|
options: viewModel.langs,
|
||||||
})
|
optionToString: { $0.name },
|
||||||
Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {}
|
selected: Binding<TrackLanguage>(
|
||||||
|
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto },
|
||||||
|
set: {autoSelectSubtitlesLangcode = $0.isoCode}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SearchablePicker(label: "Preferred audio language",
|
||||||
|
options: viewModel.langs,
|
||||||
|
optionToString: { $0.name },
|
||||||
|
selected: Binding<TrackLanguage>(
|
||||||
|
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto },
|
||||||
|
set: { autoSelectAudioLangcode = $0.isoCode}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
@ -65,6 +68,9 @@ struct SettingsView: View {
|
||||||
Text("Signed in as \(username)").foregroundColor(.primary)
|
Text("Signed in as \(username)").foregroundColor(.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
|
let nc = NotificationCenter.default
|
||||||
|
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)
|
||||||
|
@ -79,7 +85,7 @@ struct SettingsView: View {
|
||||||
Button {
|
Button {
|
||||||
close = false
|
close = false
|
||||||
} label: {
|
} label: {
|
||||||
Text("Back").font(.callout)
|
Image(systemName: "xmark")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import MediaPlayer
|
||||||
import Combine
|
import Combine
|
||||||
import GoogleCast
|
import GoogleCast
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
import Defaults
|
||||||
|
|
||||||
enum PlayerDestination {
|
enum PlayerDestination {
|
||||||
case remote
|
case remote
|
||||||
|
@ -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 {
|
||||||
|
@ -414,7 +413,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
@ -423,7 +422,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)
|
||||||
|
@ -433,9 +432,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)
|
||||||
|
@ -460,7 +459,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
|
||||||
|
@ -474,8 +473,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
mediaPlayer.drawable = videoContentView
|
mediaPlayer.drawable = videoContentView
|
||||||
|
|
||||||
// Fetch max bitrate from UserDefaults depending on current connection mode
|
// Fetch max bitrate from UserDefaults depending on current connection mode
|
||||||
let defaults = UserDefaults.standard
|
let maxBitrate = Defaults[.inNetworkBandwidth]
|
||||||
let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth")
|
|
||||||
|
|
||||||
// Build a device profile
|
// Build a device profile
|
||||||
let builder = DeviceProfileBuilder()
|
let builder = DeviceProfileBuilder()
|
||||||
|
@ -513,7 +511,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
item.videoType = .transcode
|
item.videoType = .transcode
|
||||||
item.videoUrl = streamURL!
|
item.videoUrl = streamURL!
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "")
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "")
|
||||||
subtitleTrackArray.append(disableSubtitleTrack)
|
subtitleTrackArray.append(disableSubtitleTrack)
|
||||||
|
|
||||||
// Loop through media streams and add to array
|
// Loop through media streams and add to array
|
||||||
|
@ -525,7 +523,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
} else {
|
} else {
|
||||||
deliveryUrl = nil
|
deliveryUrl = nil
|
||||||
}
|
}
|
||||||
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt")
|
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "")
|
||||||
|
|
||||||
if subtitle.delivery != .encode {
|
if subtitle.delivery != .encode {
|
||||||
subtitleTrackArray.append(subtitle)
|
subtitleTrackArray.append(subtitle)
|
||||||
|
@ -533,7 +531,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.type == .audio {
|
if stream.type == .audio {
|
||||||
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!))
|
||||||
if stream.isDefault! == true {
|
if stream.isDefault! == true {
|
||||||
selectedAudioTrack = Int32(stream.index!)
|
selectedAudioTrack = Int32(stream.index!)
|
||||||
}
|
}
|
||||||
|
@ -557,7 +555,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
item.videoUrl = streamURL
|
item.videoUrl = streamURL
|
||||||
item.videoType = .directPlay
|
item.videoType = .directPlay
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "")
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "")
|
||||||
subtitleTrackArray.append(disableSubtitleTrack)
|
subtitleTrackArray.append(disableSubtitleTrack)
|
||||||
|
|
||||||
// Loop through media streams and add to array
|
// Loop through media streams and add to array
|
||||||
|
@ -569,7 +567,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
} else {
|
} else {
|
||||||
deliveryUrl = nil
|
deliveryUrl = nil
|
||||||
}
|
}
|
||||||
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!)
|
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "")
|
||||||
|
|
||||||
if subtitle.delivery != .encode {
|
if subtitle.delivery != .encode {
|
||||||
subtitleTrackArray.append(subtitle)
|
subtitleTrackArray.append(subtitle)
|
||||||
|
@ -577,7 +575,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.type == .audio {
|
if stream.type == .audio {
|
||||||
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!))
|
||||||
if stream.isDefault! == true {
|
if stream.isDefault! == true {
|
||||||
selectedAudioTrack = Int32(stream.index!)
|
selectedAudioTrack = Int32(stream.index!)
|
||||||
}
|
}
|
||||||
|
@ -604,6 +602,28 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupTracksForPreferredDefaults() {
|
||||||
|
subtitleTrackArray.forEach { subtitle in
|
||||||
|
if Defaults[.isAutoSelectSubtitles] {
|
||||||
|
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
|
||||||
|
subtitle.languageCode.contains(Locale.current.languageCode ?? "") {
|
||||||
|
selectedCaptionTrack = subtitle.id
|
||||||
|
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
||||||
|
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
|
||||||
|
selectedCaptionTrack = subtitle.id
|
||||||
|
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTrackArray.forEach { audio in
|
||||||
|
if audio.languageCode.contains(Defaults[.autoSelectAudioLangCode]) {
|
||||||
|
selectedAudioTrack = audio.id
|
||||||
|
mediaPlayer.currentAudioTrackIndex = audio.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startLocalPlaybackEngine(_ fetchCaptions: Bool) {
|
func startLocalPlaybackEngine(_ fetchCaptions: Bool) {
|
||||||
print("Local playback engine starting.")
|
print("Local playback engine starting.")
|
||||||
mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl)
|
mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl)
|
||||||
|
@ -611,8 +631,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 {
|
||||||
|
@ -621,7 +641,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)")
|
||||||
|
@ -632,7 +652,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()
|
||||||
|
@ -655,6 +675,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
mediaPlayer.pause()
|
mediaPlayer.pause()
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
|
setupTracksForPreferredDefaults()
|
||||||
|
|
||||||
print("Local engine started.")
|
print("Local engine started.")
|
||||||
}
|
}
|
||||||
|
@ -671,15 +692,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
|
||||||
|
@ -701,19 +722,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -739,9 +760,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)
|
||||||
|
@ -755,15 +776,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()
|
||||||
|
|
||||||
|
@ -803,11 +824,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)
|
||||||
|
@ -815,15 +835,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
|
||||||
|
@ -888,18 +908,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
|
||||||
|
@ -946,7 +955,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" {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 Foundation
|
||||||
|
import Defaults
|
||||||
|
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000)
|
||||||
|
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000)
|
||||||
|
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false)
|
||||||
|
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto")
|
||||||
|
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto")
|
||||||
|
}
|
|
@ -24,14 +24,16 @@ struct ImageView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyImage(source: source)
|
LazyImage(source: source) { state in
|
||||||
.placeholder {
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
} else if state.error != nil {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray)
|
||||||
|
} else {
|
||||||
Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 16, height: 16))!)
|
Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 16, height: 16))!)
|
||||||
.resizable()
|
.resizable()
|
||||||
}
|
}
|
||||||
.failure {
|
}
|
||||||
Rectangle()
|
|
||||||
.fill(Color.gray)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
private struct SearchablePickerView<Selectable: Hashable>: View {
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
|
let options: [Selectable]
|
||||||
|
let optionToString: (Selectable) -> String
|
||||||
|
let label: String
|
||||||
|
|
||||||
|
@State var text = ""
|
||||||
|
@Binding var selected: Selectable
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
SearchBar(text: $text)
|
||||||
|
List(options.filter {
|
||||||
|
guard !text.isEmpty else { return true }
|
||||||
|
return optionToString($0).lowercased().contains(text.lowercased())
|
||||||
|
}, id: \.self) { selectable in
|
||||||
|
Button(action: {
|
||||||
|
selected = selectable
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(optionToString(selectable)).foregroundColor(Color.primary)
|
||||||
|
Spacer()
|
||||||
|
if selected == selectable {
|
||||||
|
Image(systemName: "checkmark").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.listStyle(GroupedListStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchablePicker<Selectable: Hashable>: View {
|
||||||
|
let label: String
|
||||||
|
let options: [Selectable]
|
||||||
|
let optionToString: (Selectable) -> String
|
||||||
|
|
||||||
|
@Binding var selected: Selectable
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink(destination: searchablePickerView()) {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
Spacer()
|
||||||
|
Text(optionToString(selected))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func searchablePickerView() -> some View {
|
||||||
|
SearchablePickerView(options: options,
|
||||||
|
optionToString: optionToString,
|
||||||
|
label: label,
|
||||||
|
selected: $selected)
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,11 +66,9 @@ public class ServerDiscovery {
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {
|
func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {
|
||||||
print("RECIEVED \(ipAddress):\(String(port)) \(response)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorHandler(error: UDPBroadcastConnection.ConnectionError) {
|
func errorHandler(error: UDPBroadcastConnection.ConnectionError) {
|
||||||
print(error)
|
|
||||||
}
|
}
|
||||||
self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler)
|
self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler)
|
||||||
}
|
}
|
||||||
|
@ -81,7 +79,6 @@ public class ServerDiscovery {
|
||||||
let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data)
|
let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data)
|
||||||
completion(response)
|
completion(response)
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
|
||||||
completion(nil)
|
completion(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
//
|
//
|
||||||
|
|
|
@ -18,7 +18,6 @@ struct LibraryFilters: Codable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SortBy: String, Codable, CaseIterable {
|
public enum SortBy: String, Codable, CaseIterable {
|
||||||
case productionYear = "ProductionYear"
|
|
||||||
case premiereDate = "PremiereDate"
|
case premiereDate = "PremiereDate"
|
||||||
case name = "SortName"
|
case name = "SortName"
|
||||||
case dateAdded = "DateCreated"
|
case dateAdded = "DateCreated"
|
||||||
|
@ -27,14 +26,12 @@ public enum SortBy: String, Codable, CaseIterable {
|
||||||
extension SortBy {
|
extension SortBy {
|
||||||
var localized: String {
|
var localized: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .productionYear:
|
|
||||||
return "Release Year"
|
|
||||||
case .premiereDate:
|
case .premiereDate:
|
||||||
return "Premiere date"
|
return "Premiere date"
|
||||||
case .name:
|
case .name:
|
||||||
return "Title"
|
return "Name"
|
||||||
case .dateAdded:
|
case .dateAdded:
|
||||||
return "Date Added"
|
return "Date added"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -35,10 +35,26 @@ final class LibraryFilterViewModel: ViewModel {
|
||||||
var possibleItemFilters = ItemFilter.supportedTypes
|
var possibleItemFilters = ItemFilter.supportedTypes
|
||||||
@Published
|
@Published
|
||||||
var enabledFilterType: [FilterType]
|
var enabledFilterType: [FilterType]
|
||||||
|
@Published
|
||||||
|
var selectedSortOrder: APISortOrder = .descending
|
||||||
|
@Published
|
||||||
|
var selectedSortBy: SortBy = .name
|
||||||
|
|
||||||
|
func updateModifiedFilter() {
|
||||||
|
modifiedFilters.sortOrder = [selectedSortOrder]
|
||||||
|
modifiedFilters.sortBy = [selectedSortBy]
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetFilters() {
|
||||||
|
modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||||
|
}
|
||||||
|
|
||||||
init(filters: LibraryFilters? = nil,
|
init(filters: LibraryFilters? = nil,
|
||||||
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter]) {
|
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter]) {
|
||||||
self.enabledFilterType = enabledFilterType
|
self.enabledFilterType = enabledFilterType
|
||||||
|
self.selectedSortBy = filters!.sortBy.first!
|
||||||
|
self.selectedSortOrder = filters!.sortOrder.first!
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
if let filters = filters {
|
if let filters = filters {
|
||||||
self.modifiedFilters = filters
|
self.modifiedFilters = filters
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
//
|
//
|
||||||
/*
|
/*
|
||||||
* SwiftFin is subject to the terms of the Mozilla Public
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
* 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/.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*
|
*
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@ -23,8 +23,17 @@ struct Bitrates: Codable, Hashable {
|
||||||
public var value: Int
|
public var value: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TrackLanguage: Hashable {
|
||||||
|
var name: String
|
||||||
|
var isoCode: String
|
||||||
|
|
||||||
|
static let auto = TrackLanguage(name: "Auto", isoCode: "Auto")
|
||||||
|
}
|
||||||
|
|
||||||
final class SettingsViewModel: ObservableObject {
|
final class SettingsViewModel: ObservableObject {
|
||||||
|
let currentLocale = Locale.current
|
||||||
var bitrates: [Bitrates] = []
|
var bitrates: [Bitrates] = []
|
||||||
|
var langs = [TrackLanguage]()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
|
let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
|
||||||
|
@ -39,5 +48,11 @@ final class SettingsViewModel: ObservableObject {
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.langs = Locale.isoLanguageCodes.compactMap {
|
||||||
|
guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil }
|
||||||
|
return TrackLanguage(name: name, isoCode: $0)
|
||||||
|
}.sorted(by: { $0.name < $1.name })
|
||||||
|
self.langs.insert(.auto, at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,14 +30,6 @@ final class SplashViewModel: ViewModel {
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
if defaults.integer(forKey: "InNetworkBandwidth") == 0 {
|
|
||||||
defaults.setValue(40_000_000, forKey: "InNetworkBandwidth")
|
|
||||||
}
|
|
||||||
if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 {
|
|
||||||
defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth")
|
|
||||||
}
|
|
||||||
|
|
||||||
let nc = NotificationCenter.default
|
let nc = NotificationCenter.default
|
||||||
nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil)
|
nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil)
|
||||||
nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil)
|
nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil)
|
||||||
|
|
|
@ -16,10 +16,12 @@ struct Subtitle {
|
||||||
var url: URL?
|
var url: URL?
|
||||||
var delivery: SubtitleDeliveryMethod
|
var delivery: SubtitleDeliveryMethod
|
||||||
var codec: String
|
var codec: String
|
||||||
|
var languageCode: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AudioTrack {
|
struct AudioTrack {
|
||||||
var name: String
|
var name: String
|
||||||
|
var languageCode: String
|
||||||
var id: Int32
|
var id: Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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],
|
||||||
|
|
Loading…
Reference in New Issue