commit
391bb0decf
|
@ -10,7 +10,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
private struct CutOffShadow: Shape {
|
struct CutOffShadow: Shape {
|
||||||
func path(in rect: CGRect) -> Path {
|
func path(in rect: CGRect) -> Path {
|
||||||
var path = Path()
|
var path = Path()
|
||||||
|
|
||||||
|
@ -91,22 +91,18 @@ struct LandscapeItemElement: View {
|
||||||
)
|
)
|
||||||
.shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0)
|
.shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0)
|
||||||
.shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0)
|
.shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0)
|
||||||
if focused {
|
if inSeasonView ?? false {
|
||||||
if inSeasonView ?? false {
|
Text("\(item.getEpisodeLocator() ?? "") • \(item.name ?? "")")
|
||||||
Text("\(item.getEpisodeLocator() ?? "") • \(item.name ?? "")")
|
.font(.callout)
|
||||||
.font(.callout)
|
.fontWeight(.semibold)
|
||||||
.fontWeight(.semibold)
|
.lineLimit(1)
|
||||||
.lineLimit(1)
|
.frame(width: 445)
|
||||||
.frame(width: 445)
|
|
||||||
} else {
|
|
||||||
Text(item.type == "Episode" ? "\(item.seriesName ?? "") • \(item.getEpisodeLocator() ?? "")" : item.name ?? "")
|
|
||||||
.font(.callout)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.lineLimit(1)
|
|
||||||
.frame(width: 445)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Spacer().frame(height: 25)
|
Text(item.type == "Episode" ? "\(item.seriesName ?? "") • \(item.getEpisodeLocator() ?? "")" : item.name ?? "")
|
||||||
|
.font(.callout)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(width: 445)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: envFocused) { envFocus in
|
.onChange(of: envFocused) { envFocus in
|
||||||
|
|
|
@ -10,14 +10,16 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MediaPlayButtonRowView: View {
|
struct MediaPlayButtonRowView: View {
|
||||||
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
@ObservedObject var viewModel: ItemViewModel
|
@ObservedObject var viewModel: ItemViewModel
|
||||||
@State var wrappedScrollView: UIScrollView?
|
@State var wrappedScrollView: UIScrollView?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack {
|
VStack {
|
||||||
NavigationLink(destination: VideoPlayerView(item: viewModel.item).ignoresSafeArea()) {
|
Button {
|
||||||
|
self.itemRouter.route(to: \.videoPlayer, viewModel.item)
|
||||||
|
} label: {
|
||||||
MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView)
|
MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView)
|
||||||
}
|
}
|
||||||
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : L10n.play)
|
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : L10n.play)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryListView: View {
|
struct LibraryListView: View {
|
||||||
|
@EnvironmentObject var mainCoordinator: MainCoordinator.Router
|
||||||
|
@EnvironmentObject var libraryListRouter: LibraryListCoordinator.Router
|
||||||
@StateObject var viewModel = LibraryListViewModel()
|
@StateObject var viewModel = LibraryListViewModel()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -21,12 +23,15 @@ struct LibraryListView: View {
|
||||||
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" || library.collectionType ?? "" == "music" {
|
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" || library.collectionType ?? "" == "music" {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} else {
|
} else {
|
||||||
NavigationLink(destination: LazyView {
|
Button() {
|
||||||
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "")
|
if library.collectionType == "livetv" {
|
||||||
}) {
|
self.mainCoordinator.root(\.liveTV)
|
||||||
|
} else {
|
||||||
|
self.libraryListRouter.route(to: \.library, (viewModel: LibraryViewModel(), title: library.name ?? ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
|
|
||||||
.opacity(0.4)
|
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack {
|
VStack {
|
||||||
|
@ -37,7 +42,7 @@ struct LibraryListView: View {
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}.padding(32)
|
}.padding(32)
|
||||||
}.background(Color.black)
|
}
|
||||||
.frame(minWidth: 100, maxWidth: .infinity)
|
.frame(minWidth: 100, maxWidth: .infinity)
|
||||||
.frame(height: 100)
|
.frame(height: 100)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftUICollection
|
||||||
|
|
||||||
|
|
||||||
|
struct LiveTVChannelsView: View {
|
||||||
|
@EnvironmentObject var router: LiveTVChannelsCoordinator.Router
|
||||||
|
@StateObject var viewModel = LiveTVChannelsViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if viewModel.isLoading == true {
|
||||||
|
ProgressView()
|
||||||
|
} else if !viewModel.rows.isEmpty {
|
||||||
|
CollectionView(rows: viewModel.rows) { section, env in
|
||||||
|
return createGridLayout()
|
||||||
|
} cell: { indexPath, cell in
|
||||||
|
makeCellView(indexPath: indexPath, cell: cell)
|
||||||
|
} supplementaryView: { _, indexPath in
|
||||||
|
EmptyView()
|
||||||
|
.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onAppear {
|
||||||
|
viewModel.startScheduleCheckTimer()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
viewModel.stopScheduleCheckTimer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack {
|
||||||
|
Text("No results.")
|
||||||
|
Button {
|
||||||
|
viewModel.getChannels()
|
||||||
|
} label: {
|
||||||
|
Text("Reload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
|
||||||
|
let item = cell.item
|
||||||
|
let channel = item.channel
|
||||||
|
if channel.type != "Folder" {
|
||||||
|
Button {
|
||||||
|
self.router.route(to: \.videoPlayer, channel)
|
||||||
|
} label: {
|
||||||
|
LiveTVChannelItemElement(
|
||||||
|
channel: channel,
|
||||||
|
program: item.program,
|
||||||
|
startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ",
|
||||||
|
endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ",
|
||||||
|
progressPercent: item.program?.getLiveProgressPercentage() ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createGridLayout() -> NSCollectionLayoutSection {
|
||||||
|
// I don't know why tvOS has a margin on the sides of a collection view
|
||||||
|
// But it does, even with contentInset = .zero and ignoreSafeArea.
|
||||||
|
let sideMargin = CGFloat(30)
|
||||||
|
let itemWidth = (UIScreen.main.bounds.width / 4) - (sideMargin * 2)
|
||||||
|
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(itemWidth),
|
||||||
|
heightDimension: .absolute(itemWidth))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
item.edgeSpacing = .init(
|
||||||
|
leading: .fixed(8),
|
||||||
|
top: .fixed(8),
|
||||||
|
trailing: .fixed(8),
|
||||||
|
bottom: .fixed(8)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
|
||||||
|
heightDimension: .absolute(itemWidth))
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
|
||||||
|
subitems: [item])
|
||||||
|
group.edgeSpacing = .init(
|
||||||
|
leading: .fixed(0),
|
||||||
|
top: .fixed(16),
|
||||||
|
trailing: .fixed(0),
|
||||||
|
bottom: .fixed(16)
|
||||||
|
)
|
||||||
|
group.contentInsets = .zero
|
||||||
|
|
||||||
|
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
section.contentInsets = .zero
|
||||||
|
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
struct LiveTVHomeView: View {
|
||||||
|
@EnvironmentObject var mainCoordinator: MainCoordinator.Router
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {} label: {
|
||||||
|
Text("Return Home")
|
||||||
|
}.onAppear {
|
||||||
|
self.mainCoordinator.root(\.mainTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
struct LiveTVProgramsView: View {
|
||||||
|
@EnvironmentObject var programsRouter: LiveTVProgramsCoordinator.Router
|
||||||
|
@StateObject var viewModel = LiveTVProgramsViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading) {
|
||||||
|
if !viewModel.recommendedItems.isEmpty,
|
||||||
|
let items = viewModel.recommendedItems {
|
||||||
|
Text("On Now")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.padding(.leading, 90)
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(items, id: \.id) { item in
|
||||||
|
Button {
|
||||||
|
if let chanId = item.channelId,
|
||||||
|
let chan = viewModel.findChannel(id: chanId) {
|
||||||
|
self.programsRouter.route(to: \.videoPlayer, chan)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
LandscapeItemElement(item: item)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.frame(height: 350)
|
||||||
|
}
|
||||||
|
if !viewModel.seriesItems.isEmpty,
|
||||||
|
let items = viewModel.seriesItems {
|
||||||
|
Text("Shows")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.padding(.leading, 90)
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(items, id: \.id) { item in
|
||||||
|
Button {
|
||||||
|
if let chanId = item.channelId,
|
||||||
|
let chan = viewModel.findChannel(id: chanId) {
|
||||||
|
self.programsRouter.route(to: \.videoPlayer, chan)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
LandscapeItemElement(item: item)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.frame(height: 350)
|
||||||
|
}
|
||||||
|
if !viewModel.movieItems.isEmpty,
|
||||||
|
let items = viewModel.movieItems {
|
||||||
|
Text("Movies")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.padding(.leading, 90)
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(items, id: \.id) { item in
|
||||||
|
Button {
|
||||||
|
if let chanId = item.channelId,
|
||||||
|
let chan = viewModel.findChannel(id: chanId) {
|
||||||
|
self.programsRouter.route(to: \.videoPlayer, chan)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
LandscapeItemElement(item: item)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.frame(height: 350)
|
||||||
|
}
|
||||||
|
if !viewModel.sportsItems.isEmpty,
|
||||||
|
let items = viewModel.sportsItems {
|
||||||
|
Text("Sports")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.padding(.leading, 90)
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(items, id: \.id) { item in
|
||||||
|
Button {
|
||||||
|
if let chanId = item.channelId,
|
||||||
|
let chan = viewModel.findChannel(id: chanId) {
|
||||||
|
self.programsRouter.route(to: \.videoPlayer, chan)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
LandscapeItemElement(item: item)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.frame(height: 350)
|
||||||
|
}
|
||||||
|
if !viewModel.kidsItems.isEmpty,
|
||||||
|
let items = viewModel.kidsItems {
|
||||||
|
Text("Kids")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.padding(.leading, 90)
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(items, id: \.id) { item in
|
||||||
|
Button {
|
||||||
|
if let chanId = item.channelId,
|
||||||
|
let chan = viewModel.findChannel(id: chanId) {
|
||||||
|
self.programsRouter.route(to: \.videoPlayer, chan)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
LandscapeItemElement(item: item)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.frame(height: 350)
|
||||||
|
}
|
||||||
|
if !viewModel.newsItems.isEmpty,
|
||||||
|
let items = viewModel.newsItems {
|
||||||
|
Text("News")
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.padding(.leading, 90)
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
ForEach(items, id: \.id) { item in
|
||||||
|
Button {
|
||||||
|
if let chanId = item.channelId,
|
||||||
|
let chan = viewModel.findChannel(id: chanId) {
|
||||||
|
self.programsRouter.route(to: \.videoPlayer, chan)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
LandscapeItemElement(item: item)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||||
|
}
|
||||||
|
Spacer().frame(width: 45)
|
||||||
|
}
|
||||||
|
}.frame(height: 350)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,10 +7,13 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct VideoPlayerView: UIViewControllerRepresentable {
|
struct VideoPlayerView: UIViewControllerRepresentable {
|
||||||
|
@EnvironmentObject var router: VideoPlayerCoordinator.Router
|
||||||
|
|
||||||
var item: BaseItemDto
|
var item: BaseItemDto
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> some UIViewController {
|
func makeUIViewController(context: Context) -> some UIViewController {
|
||||||
|
@ -18,6 +21,9 @@ struct VideoPlayerView: UIViewControllerRepresentable {
|
||||||
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
||||||
let viewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! VideoPlayerViewController
|
let viewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! VideoPlayerViewController
|
||||||
viewController.manifest = item
|
viewController.manifest = item
|
||||||
|
viewController.backAction = {
|
||||||
|
self.router.dismissCoordinator()
|
||||||
|
}
|
||||||
|
|
||||||
return viewController
|
return viewController
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
var manifest: BaseItemDto = BaseItemDto()
|
var manifest: BaseItemDto = BaseItemDto()
|
||||||
var playbackItem = PlaybackItem()
|
var playbackItem = PlaybackItem()
|
||||||
var playSessionId: String = ""
|
var playSessionId: String = ""
|
||||||
|
var backAction = {}
|
||||||
|
|
||||||
var cancellables = Set<AnyCancellable>()
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -486,7 +487,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
self.resignFirstResponder()
|
self.resignFirstResponder()
|
||||||
mediaPlayer.stop()
|
mediaPlayer.stop()
|
||||||
sendStopReport()
|
sendStopReport()
|
||||||
self.navigationController?.popViewController(animated: true)
|
backAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,7 +583,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
updateNowPlayingCenter(time: nil, playing: mediaPlayer.state == .playing)
|
updateNowPlayingCenter(time: nil, playing: mediaPlayer.state == .playing)
|
||||||
|
|
||||||
if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" {
|
if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" {
|
||||||
var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!))
|
var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks ?? 0))
|
||||||
if ticks == 0 {
|
if ticks == 0 {
|
||||||
ticks = manifest.userData?.playbackPositionTicks ?? 0
|
ticks = manifest.userData?.playbackPositionTicks ?? 0
|
||||||
}
|
}
|
||||||
|
@ -600,7 +601,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendStopReport() {
|
func sendStopReport() {
|
||||||
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
|
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks ?? 0)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
||||||
.sink(receiveCompletion: { result in
|
.sink(receiveCompletion: { result in
|
||||||
|
|
|
@ -234,6 +234,9 @@
|
||||||
C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
|
C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
|
||||||
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
|
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
|
||||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||||
|
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; };
|
||||||
|
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */; };
|
||||||
|
C4AE2C3327498DBE00AE13CF /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
|
||||||
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; };
|
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; };
|
||||||
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; };
|
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; };
|
||||||
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; };
|
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; };
|
||||||
|
@ -242,8 +245,23 @@
|
||||||
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; };
|
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; };
|
||||||
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */; };
|
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */; };
|
||||||
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; };
|
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; };
|
||||||
|
C4BE07712725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
|
||||||
|
C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
|
||||||
|
C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */; };
|
||||||
|
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; };
|
||||||
|
C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; };
|
||||||
|
C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; };
|
||||||
|
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; };
|
||||||
|
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; };
|
||||||
|
C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; };
|
||||||
|
C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
||||||
|
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
||||||
|
C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
||||||
|
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
||||||
|
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; };
|
||||||
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
||||||
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
||||||
|
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
|
||||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
||||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||||
|
@ -561,12 +579,25 @@
|
||||||
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = "<group>"; };
|
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = "<group>"; };
|
||||||
C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = "<group>"; };
|
C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = "<group>"; };
|
||||||
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = "<group>"; };
|
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = "<group>"; };
|
||||||
|
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = "<group>"; };
|
||||||
|
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = "<group>"; };
|
||||||
C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesCoordinator.swift; sourceTree = "<group>"; };
|
C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesCoordinator.swift; sourceTree = "<group>"; };
|
||||||
C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = "<group>"; };
|
C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = "<group>"; };
|
||||||
C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = "<group>"; };
|
C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = "<group>"; };
|
||||||
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = "<group>"; };
|
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVTabCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = "<group>"; };
|
||||||
|
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = "<group>"; };
|
||||||
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||||
|
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||||
|
D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||||
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
|
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
@ -721,7 +752,9 @@
|
||||||
625CB5742678C33500530A6E /* LibraryListViewModel.swift */,
|
625CB5742678C33500530A6E /* LibraryListViewModel.swift */,
|
||||||
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */,
|
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */,
|
||||||
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */,
|
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */,
|
||||||
|
C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */,
|
||||||
536D3D75267BA9BB0004248C /* MainTabViewModel.swift */,
|
536D3D75267BA9BB0004248C /* MainTabViewModel.swift */,
|
||||||
|
C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */,
|
||||||
C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */,
|
C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */,
|
||||||
C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */,
|
C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */,
|
||||||
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */,
|
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */,
|
||||||
|
@ -1117,6 +1150,9 @@
|
||||||
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
||||||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
||||||
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
||||||
|
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */,
|
||||||
|
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */,
|
||||||
|
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
|
||||||
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
|
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
|
||||||
E193D5412719404B00900D82 /* MainCoordinator */,
|
E193D5412719404B00900D82 /* MainCoordinator */,
|
||||||
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
||||||
|
@ -1194,6 +1230,9 @@
|
||||||
C4E508172703E8190045C9AB /* LibraryListView.swift */,
|
C4E508172703E8190045C9AB /* LibraryListView.swift */,
|
||||||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */,
|
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */,
|
||||||
53A83C32268A309300DF3D92 /* LibraryView.swift */,
|
53A83C32268A309300DF3D92 /* LibraryView.swift */,
|
||||||
|
C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */,
|
||||||
|
C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */,
|
||||||
|
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */,
|
||||||
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */,
|
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */,
|
||||||
C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */,
|
C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */,
|
||||||
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
|
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
|
||||||
|
@ -1236,6 +1275,8 @@
|
||||||
625CB56E2678C23300530A6E /* HomeView.swift */,
|
625CB56E2678C23300530A6E /* HomeView.swift */,
|
||||||
E14F7D0A26DB3714007C3AE6 /* ItemView */,
|
E14F7D0A26DB3714007C3AE6 /* ItemView */,
|
||||||
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */,
|
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */,
|
||||||
|
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
||||||
|
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
||||||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
|
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
|
||||||
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
||||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
||||||
|
@ -1331,6 +1372,7 @@
|
||||||
E1AD105326D96F5A003E4A08 /* Views */ = {
|
E1AD105326D96F5A003E4A08 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
|
||||||
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
|
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
|
||||||
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
||||||
621338B22660A07800A81A2A /* LazyView.swift */,
|
621338B22660A07800A81A2A /* LazyView.swift */,
|
||||||
|
@ -1809,6 +1851,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */,
|
E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */,
|
||||||
|
C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */,
|
||||||
531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */,
|
531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */,
|
||||||
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */,
|
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */,
|
||||||
E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */,
|
E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */,
|
||||||
|
@ -1830,6 +1873,7 @@
|
||||||
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
||||||
53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */,
|
53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */,
|
||||||
E193D53E27193F9A00900D82 /* VideoPlayerCoordinator.swift in Sources */,
|
E193D53E27193F9A00900D82 /* VideoPlayerCoordinator.swift in Sources */,
|
||||||
|
C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
||||||
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
|
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
|
||||||
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
||||||
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
||||||
|
@ -1841,6 +1885,7 @@
|
||||||
E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||||
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
|
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||||
62671DB327159C1800199D95 /* ItemCoordinator.swift in Sources */,
|
62671DB327159C1800199D95 /* ItemCoordinator.swift in Sources */,
|
||||||
|
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */,
|
||||||
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */,
|
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */,
|
||||||
E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
||||||
53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */,
|
53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */,
|
||||||
|
@ -1861,17 +1906,21 @@
|
||||||
E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */,
|
E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */,
|
||||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */,
|
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */,
|
||||||
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
|
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
|
||||||
|
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */,
|
||||||
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
||||||
E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
|
E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
|
||||||
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
|
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
|
||||||
|
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
|
||||||
E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */,
|
E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */,
|
||||||
53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */,
|
53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */,
|
||||||
531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */,
|
531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */,
|
||||||
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
|
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
|
||||||
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
||||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||||
|
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */,
|
||||||
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
|
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
|
||||||
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */,
|
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */,
|
||||||
|
C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
||||||
E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||||
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||||
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
|
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
|
||||||
|
@ -1903,6 +1952,7 @@
|
||||||
E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */,
|
E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */,
|
||||||
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
||||||
|
C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */,
|
||||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||||
6264E88D273850380081A12A /* Strings.swift in Sources */,
|
6264E88D273850380081A12A /* Strings.swift in Sources */,
|
||||||
|
@ -1924,6 +1974,7 @@
|
||||||
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
||||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||||
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
|
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
|
||||||
|
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
|
||||||
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||||
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
||||||
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */,
|
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */,
|
||||||
|
@ -1959,7 +2010,9 @@
|
||||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||||
|
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
|
C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
|
||||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||||
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
||||||
|
@ -1968,6 +2021,7 @@
|
||||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
||||||
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
||||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||||
|
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */,
|
||||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
||||||
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
|
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
|
||||||
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */,
|
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */,
|
||||||
|
@ -1979,6 +2033,7 @@
|
||||||
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
||||||
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
||||||
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
||||||
|
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
|
||||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
||||||
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||||
|
@ -2002,18 +2057,22 @@
|
||||||
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
|
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
|
||||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
||||||
|
C4AE2C3327498DBE00AE13CF /* LiveTVChannelItemElement.swift in Sources */,
|
||||||
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
|
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
|
||||||
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
|
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
|
||||||
|
C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
|
||||||
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
||||||
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
||||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
||||||
6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
|
6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
|
||||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||||
|
C4BE07712725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */,
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||||
|
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
||||||
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
||||||
E13DD3BD27163C63009D4DAF /* EmailHelper.swift in Sources */,
|
E13DD3BD27163C63009D4DAF /* EmailHelper.swift in Sources */,
|
||||||
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||||
|
@ -2035,6 +2094,7 @@
|
||||||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
||||||
|
C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */,
|
||||||
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */,
|
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */,
|
||||||
E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */,
|
E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */,
|
||||||
62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1300"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "5358705F2669D21600D05A09"
|
||||||
|
BuildableName = "JellyfinPlayer tvOS.app"
|
||||||
|
BlueprintName = "JellyfinPlayer tvOS"
|
||||||
|
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "5358705F2669D21600D05A09"
|
||||||
|
BuildableName = "JellyfinPlayer tvOS.app"
|
||||||
|
BlueprintName = "JellyfinPlayer tvOS"
|
||||||
|
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "5358705F2669D21600D05A09"
|
||||||
|
BuildableName = "JellyfinPlayer tvOS.app"
|
||||||
|
BlueprintName = "JellyfinPlayer tvOS"
|
||||||
|
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -87,8 +87,8 @@
|
||||||
"repositoryURL": "https://github.com/rundfunk47/stinsen",
|
"repositoryURL": "https://github.com/rundfunk47/stinsen",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "3d06c7603c70f8af1bd49f8d49f17e98f25b2d6a",
|
"revision": "5e6c714f6f308877c8a988523915f9eb592d7d82",
|
||||||
"version": "2.0.2"
|
"version": "2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* JellyfinPlayer/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 Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveTVHomeView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("Coming Soon")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* JellyfinPlayer/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 Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LiveTVProgramsView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("Coming Soon")
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,12 @@ final class LibraryListCoordinator: NavigationCoordinatable {
|
||||||
@Route(.push) var search = makeSearch
|
@Route(.push) var search = makeSearch
|
||||||
@Route(.push) var library = makeLibrary
|
@Route(.push) var library = makeLibrary
|
||||||
|
|
||||||
|
let viewModel: LibraryListViewModel
|
||||||
|
|
||||||
|
init(viewModel: LibraryListViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
||||||
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +35,6 @@ final class LibraryListCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
func makeStart() -> some View {
|
||||||
LibraryListView()
|
LibraryListView(viewModel: self.viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 JellyfinAPI
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class LiveTVChannelsCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.modal) var modalItem = makeModalItem
|
||||||
|
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
|
||||||
|
|
||||||
|
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
||||||
|
return NavigationViewCoordinator(ItemCoordinator(item: item))
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
|
||||||
|
NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeStart() -> some View {
|
||||||
|
LiveTVChannelsView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 JellyfinAPI
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class LiveTVProgramsCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
|
let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
|
||||||
|
|
||||||
|
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
|
||||||
|
NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeStart() -> some View {
|
||||||
|
LiveTVProgramsView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import Stinsen
|
||||||
|
|
||||||
|
final class LiveTVTabCoordinator: TabCoordinatable {
|
||||||
|
var child = TabChild(startingItems: [
|
||||||
|
\LiveTVTabCoordinator.programs,
|
||||||
|
\LiveTVTabCoordinator.channels,
|
||||||
|
\LiveTVTabCoordinator.home
|
||||||
|
])
|
||||||
|
|
||||||
|
@Route(tabItem: makeProgramsTab) var programs = makePrograms
|
||||||
|
@Route(tabItem: makeChannelsTab) var channels = makeChannels
|
||||||
|
@Route(tabItem: makeHomeTab) var home = makeHome
|
||||||
|
|
||||||
|
func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> {
|
||||||
|
return NavigationViewCoordinator(LiveTVProgramsCoordinator())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeProgramsTab(isActive: Bool) -> some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "tv")
|
||||||
|
Text("Programs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> {
|
||||||
|
return NavigationViewCoordinator(LiveTVChannelsCoordinator())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeChannelsTab(isActive: Bool) -> some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "square.grid.3x3")
|
||||||
|
Text("Channels")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHome() -> LiveTVHomeView {
|
||||||
|
return LiveTVHomeView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "house")
|
||||||
|
Text("Home")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ final class MainTabCoordinator: TabCoordinatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||||
return NavigationViewCoordinator(LibraryListCoordinator())
|
return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View {
|
@ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View {
|
||||||
|
|
|
@ -17,6 +17,7 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
@Root var mainTab = makeMainTab
|
@Root var mainTab = makeMainTab
|
||||||
@Root var serverList = makeServerList
|
@Root var serverList = makeServerList
|
||||||
|
@Root var liveTV = makeLiveTV
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if SessionManager.main.currentLogin != nil {
|
if SessionManager.main.currentLogin != nil {
|
||||||
|
@ -51,4 +52,8 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
|
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
|
||||||
NavigationViewCoordinator(ServerListCoordinator())
|
NavigationViewCoordinator(ServerListCoordinator())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeLiveTV() -> LiveTVTabCoordinator {
|
||||||
|
LiveTVTabCoordinator()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ final class MainTabCoordinator: TabCoordinatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeOther() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
func makeOther() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||||
return NavigationViewCoordinator(LibraryListCoordinator())
|
return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func makeOtherTab(isActive: Bool) -> some View {
|
@ViewBuilder func makeOtherTab(isActive: Bool) -> some View {
|
||||||
|
|
|
@ -26,5 +26,6 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
@ViewBuilder func makeStart() -> some View {
|
@ViewBuilder func makeStart() -> some View {
|
||||||
VideoPlayerView(item: item)
|
VideoPlayerView(item: item)
|
||||||
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,6 +171,33 @@ public extension BaseItemDto {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLiveStartTimeString(formatter: DateFormatter) -> String {
|
||||||
|
if let startDate = self.startDate {
|
||||||
|
return formatter.string(from: startDate)
|
||||||
|
}
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLiveEndTimeString(formatter: DateFormatter) -> String {
|
||||||
|
if let endDate = self.endDate {
|
||||||
|
return formatter.string(from: endDate)
|
||||||
|
}
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLiveProgressPercentage() -> Double {
|
||||||
|
if let startDate = self.startDate,
|
||||||
|
let endDate = self.endDate {
|
||||||
|
let start = startDate.timeIntervalSinceReferenceDate
|
||||||
|
let end = endDate.timeIntervalSinceReferenceDate
|
||||||
|
let now = Date().timeIntervalSinceReferenceDate
|
||||||
|
let length = end - start
|
||||||
|
let progress = now - start
|
||||||
|
return progress / length
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: ItemType
|
// MARK: ItemType
|
||||||
|
|
||||||
enum ItemType: String {
|
enum ItemType: String {
|
||||||
|
|
|
@ -0,0 +1,229 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 JellyfinAPI
|
||||||
|
import SwiftUICollection
|
||||||
|
|
||||||
|
typealias LiveTVChannelRow = CollectionRow<Int, LiveTVChannelRowCell>
|
||||||
|
|
||||||
|
struct LiveTVChannelRowCell: Hashable {
|
||||||
|
let id = UUID()
|
||||||
|
let item: LiveTVChannelProgram
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveTVChannelProgram: Hashable {
|
||||||
|
let id = UUID()
|
||||||
|
let channel: BaseItemDto
|
||||||
|
let program: BaseItemDto?
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LiveTVChannelsViewModel: ViewModel {
|
||||||
|
|
||||||
|
@Published var channels = [BaseItemDto]()
|
||||||
|
@Published var channelPrograms = [LiveTVChannelProgram]() {
|
||||||
|
didSet {
|
||||||
|
rows = []
|
||||||
|
let rowChannels = channelPrograms.chunked(into: 4)
|
||||||
|
for (index, rowChans) in rowChannels.enumerated() {
|
||||||
|
rows.append(LiveTVChannelRow(section: index, items: rowChans.map { LiveTVChannelRowCell(item: $0) }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Published var rows = [LiveTVChannelRow]()
|
||||||
|
|
||||||
|
private var programs = [BaseItemDto]()
|
||||||
|
private var channelProgramsList = [BaseItemDto: [BaseItemDto]]()
|
||||||
|
private var timer: Timer?
|
||||||
|
|
||||||
|
var timeFormatter: DateFormatter {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateFormat = "h:mm"
|
||||||
|
return df
|
||||||
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
getChannels()
|
||||||
|
startScheduleCheckTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
stopScheduleCheckTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getGuideInfo() {
|
||||||
|
LiveTvAPI.getGuideInfo()
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received Guide Info")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.getChannels()
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChannels() {
|
||||||
|
LiveTvAPI.getLiveTvChannels(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
startIndex: 0,
|
||||||
|
limit: 1000,
|
||||||
|
enableImageTypes: [.primary],
|
||||||
|
enableUserData: false,
|
||||||
|
enableFavoriteSorting: true
|
||||||
|
)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.channels = response.items ?? []
|
||||||
|
self.getPrograms()
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getPrograms() {
|
||||||
|
// http://192.168.1.50:8096/LiveTv/Programs
|
||||||
|
guard channels.count > 0 else {
|
||||||
|
LogManager.shared.log.debug("Cannot get programs, channels list empty. ")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let channelIds = channels.compactMap { $0.id }
|
||||||
|
|
||||||
|
let minEndDate = Date.now.addComponentsToDate(hours: -1)
|
||||||
|
let maxStartDate = minEndDate.addComponentsToDate(hours: 6)
|
||||||
|
|
||||||
|
let getProgramsDto = GetProgramsDto(
|
||||||
|
channelIds: channelIds,
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
maxStartDate: maxStartDate,
|
||||||
|
minEndDate: minEndDate,
|
||||||
|
sortBy: ["StartDate"],
|
||||||
|
enableImages: true,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [.primary],
|
||||||
|
enableUserData: false
|
||||||
|
)
|
||||||
|
|
||||||
|
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Programs")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.programs = response.items ?? []
|
||||||
|
self.channelPrograms = self.processChannelPrograms()
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processChannelPrograms() -> [LiveTVChannelProgram] {
|
||||||
|
var channelPrograms = [LiveTVChannelProgram]()
|
||||||
|
let now = Date()
|
||||||
|
for channel in self.channels {
|
||||||
|
let prgs = self.programs.filter { item in
|
||||||
|
item.channelId == channel.id
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.channelProgramsList[channel] = prgs
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPrg: BaseItemDto?
|
||||||
|
for prg in prgs {
|
||||||
|
if let startDate = prg.startDate,
|
||||||
|
let endDate = prg.endDate,
|
||||||
|
now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate &&
|
||||||
|
now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate {
|
||||||
|
currentPrg = prg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg))
|
||||||
|
}
|
||||||
|
return channelPrograms
|
||||||
|
}
|
||||||
|
|
||||||
|
func startScheduleCheckTimer() {
|
||||||
|
let date = Date()
|
||||||
|
let calendar = Calendar.current
|
||||||
|
var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute], from: date)
|
||||||
|
|
||||||
|
// Run on 10th min of every hour
|
||||||
|
guard let minute = components.minute else { return }
|
||||||
|
components.second = 0
|
||||||
|
components.minute = minute + (10 - (minute % 10))
|
||||||
|
|
||||||
|
guard let nextMinute = calendar.date(from: components) else { return }
|
||||||
|
|
||||||
|
if let existingTimer = timer {
|
||||||
|
existingTimer.invalidate()
|
||||||
|
}
|
||||||
|
timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] timer in
|
||||||
|
guard let self = self else { return }
|
||||||
|
LogManager.shared.log.debug("LiveTVChannels schedule check...")
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
let newChanPrgs = self.processChannelPrograms()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.channelPrograms = newChanPrgs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let timer = timer {
|
||||||
|
RunLoop.main.add(timer, forMode: .default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopScheduleCheckTimer() {
|
||||||
|
timer?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array {
|
||||||
|
func chunked(into size: Int) -> [[Element]] {
|
||||||
|
return stride(from: 0, to: count, by: size).map {
|
||||||
|
Array(self[$0 ..< Swift.min($0 + size, count)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
func addComponentsToDate(seconds sec: Int? = nil, minutes min: Int? = nil, hours hrs: Int? = nil, days d: Int? = nil) -> Date {
|
||||||
|
var dc = DateComponents()
|
||||||
|
if let sec = sec {
|
||||||
|
dc.second = sec
|
||||||
|
}
|
||||||
|
if let min = min {
|
||||||
|
dc.minute = min
|
||||||
|
}
|
||||||
|
if let hrs = hrs {
|
||||||
|
dc.hour = hrs
|
||||||
|
}
|
||||||
|
if let d = d {
|
||||||
|
dc.day = d
|
||||||
|
}
|
||||||
|
return Calendar.current.date(byAdding: dc, to: self)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func midnightUTCDate() -> Date {
|
||||||
|
var dc: DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: self)
|
||||||
|
dc.hour = 0
|
||||||
|
dc.minute = 0
|
||||||
|
dc.second = 0
|
||||||
|
dc.nanosecond = 0
|
||||||
|
dc.timeZone = TimeZone(secondsFromGMT: 0)
|
||||||
|
return Calendar.current.date(from: dc)!
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 JellyfinAPI
|
||||||
|
|
||||||
|
final class LiveTVProgramsViewModel: ViewModel {
|
||||||
|
|
||||||
|
@Published var recommendedItems = [BaseItemDto]()
|
||||||
|
@Published var seriesItems = [BaseItemDto]()
|
||||||
|
@Published var movieItems = [BaseItemDto]()
|
||||||
|
@Published var sportsItems = [BaseItemDto]()
|
||||||
|
@Published var kidsItems = [BaseItemDto]()
|
||||||
|
@Published var newsItems = [BaseItemDto]()
|
||||||
|
|
||||||
|
private var channels = [String:BaseItemDto]()
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
getChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findChannel(id: String) -> BaseItemDto? {
|
||||||
|
return channels[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getChannels() {
|
||||||
|
LiveTvAPI.getLiveTvChannels(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
startIndex: 0,
|
||||||
|
limit: 1000,
|
||||||
|
enableImageTypes: [.primary],
|
||||||
|
enableUserData: false,
|
||||||
|
enableFavoriteSorting: true
|
||||||
|
)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels")
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let chans = response.items {
|
||||||
|
for chan in chans {
|
||||||
|
if let chanId = chan.id {
|
||||||
|
self.channels[chanId] = chan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.getRecommendedPrograms()
|
||||||
|
self.getSeries()
|
||||||
|
self.getMovies()
|
||||||
|
self.getSports()
|
||||||
|
self.getKids()
|
||||||
|
self.getNews()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getRecommendedPrograms() {
|
||||||
|
LiveTvAPI.getRecommendedPrograms(
|
||||||
|
userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
limit: 9,
|
||||||
|
isAiring: true,
|
||||||
|
imageTypeLimit: 1,
|
||||||
|
enableImageTypes: [.primary, .thumb],
|
||||||
|
fields: [.channelInfo, .primaryImageAspectRatio],
|
||||||
|
enableTotalRecordCount: false
|
||||||
|
)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Recommended Programs")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.recommendedItems = response.items ?? []
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSeries() {
|
||||||
|
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
hasAired: false,
|
||||||
|
isMovie: false,
|
||||||
|
isSeries: true,
|
||||||
|
isNews: false,
|
||||||
|
isKids: false,
|
||||||
|
isSports: false,
|
||||||
|
limit: 9,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
enableImageTypes: [.primary, .thumb],
|
||||||
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
|
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Series Items")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.seriesItems = response.items ?? []
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getMovies() {
|
||||||
|
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
hasAired: false,
|
||||||
|
isMovie: true,
|
||||||
|
isSeries: false,
|
||||||
|
isNews: false,
|
||||||
|
isKids: false,
|
||||||
|
isSports: false,
|
||||||
|
limit: 9,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
enableImageTypes: [.primary, .thumb],
|
||||||
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
|
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Movie Items")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.movieItems = response.items ?? []
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSports() {
|
||||||
|
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
hasAired: false,
|
||||||
|
isSports: true,
|
||||||
|
limit: 9,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
enableImageTypes: [.primary, .thumb],
|
||||||
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
|
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Sports Items")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.sportsItems = response.items ?? []
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getKids() {
|
||||||
|
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
hasAired: false,
|
||||||
|
isKids: true,
|
||||||
|
limit: 9,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
enableImageTypes: [.primary, .thumb],
|
||||||
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
|
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Kids Items")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.kidsItems = response.items ?? []
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getNews() {
|
||||||
|
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
|
||||||
|
hasAired: false,
|
||||||
|
isNews: true,
|
||||||
|
limit: 9,
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
enableImageTypes: [.primary, .thumb],
|
||||||
|
fields: [.channelInfo, .primaryImageAspectRatio]
|
||||||
|
)
|
||||||
|
|
||||||
|
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
|
||||||
|
.trackActivity(loading)
|
||||||
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
self?.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { [weak self] response in
|
||||||
|
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) News Items")
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.newsItems = response.items ?? []
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
struct LiveTVChannelItemElement: View {
|
||||||
|
@Environment(\.isFocused) var envFocused: Bool
|
||||||
|
@State var focused: Bool = false
|
||||||
|
|
||||||
|
var channel: BaseItemDto
|
||||||
|
var program: BaseItemDto?
|
||||||
|
var startString = " "
|
||||||
|
var endString = " "
|
||||||
|
var progressPercent = Double(0)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(channel.number ?? "")
|
||||||
|
.font(.footnote)
|
||||||
|
.frame(alignment: .trailing)
|
||||||
|
}.frame(alignment: .top)
|
||||||
|
ImageView(src: channel.getPrimaryImage(maxWidth: 125))
|
||||||
|
.frame(width: 125, alignment: .center)
|
||||||
|
.offset(x: 0, y: -32)
|
||||||
|
Text(channel.name ?? "?")
|
||||||
|
.font(.footnote)
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(alignment: .center)
|
||||||
|
Text(program?.name ?? "N/A")
|
||||||
|
.font(.body)
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text(startString)
|
||||||
|
.font(.footnote)
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(alignment: .leading)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(endString)
|
||||||
|
.font(.footnote)
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(alignment: .trailing)
|
||||||
|
}
|
||||||
|
GeometryReader { gp in
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color.gray)
|
||||||
|
.opacity(0.4)
|
||||||
|
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12)
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||||
|
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.clear)
|
||||||
|
.border(focused ? Color.blue : Color.clear, width: 4)
|
||||||
|
.onChange(of: envFocused) { envFocus in
|
||||||
|
withAnimation(.linear(duration: 0.15)) {
|
||||||
|
self.focused = envFocus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scaleEffect(focused ? 1.1 : 1)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue