Merge pull request #426 from jhays/jhays/ios-livetv
LiveTV support for iOS
This commit is contained in:
commit
8693326d6e
|
@ -20,6 +20,10 @@ final class LibraryListCoordinator: NavigationCoordinatable {
|
|||
var search = makeSearch
|
||||
@Route(.push)
|
||||
var library = makeLibrary
|
||||
#if os(iOS)
|
||||
@Route(.push)
|
||||
var liveTV = makeLiveTV
|
||||
#endif
|
||||
|
||||
let viewModel: LibraryListViewModel
|
||||
|
||||
|
@ -35,6 +39,12 @@ final class LibraryListCoordinator: NavigationCoordinatable {
|
|||
SearchCoordinator(viewModel: viewModel)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
func makeLiveTV() -> LiveTVCoordinator {
|
||||
LiveTVCoordinator()
|
||||
}
|
||||
#endif
|
||||
|
||||
@ViewBuilder
|
||||
func makeStart() -> some View {
|
||||
LibraryListView(viewModel: self.viewModel)
|
||||
|
|
|
@ -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 (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class LiveTVCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \LiveTVCoordinator.start)
|
||||
|
||||
@Root
|
||||
var start = makeStart
|
||||
@Route(.fullScreen)
|
||||
var videoPlayer = makeVideoPlayer
|
||||
|
||||
@ViewBuilder
|
||||
func makeStart() -> some View {
|
||||
LiveTVChannelsView()
|
||||
}
|
||||
|
||||
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> {
|
||||
NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel))
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable {
|
|||
|
||||
@Root
|
||||
var start = makeStart
|
||||
@Root
|
||||
var rootLibrary = makeRootLibrary
|
||||
@Route(.push)
|
||||
var library = makeLibrary
|
||||
|
||||
|
@ -36,4 +38,8 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable {
|
|||
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
||||
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
|
||||
}
|
||||
|
||||
func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
||||
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ final class TVLibrariesCoordinator: NavigationCoordinatable {
|
|||
|
||||
@Root
|
||||
var start = makeStart
|
||||
@Root
|
||||
var rootLibrary = makeRootLibrary
|
||||
@Route(.push)
|
||||
var library = makeLibrary
|
||||
|
||||
|
@ -36,4 +38,8 @@ final class TVLibrariesCoordinator: NavigationCoordinatable {
|
|||
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
||||
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
|
||||
}
|
||||
|
||||
func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
||||
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable {
|
||||
|
||||
let stack = NavigationStack(initial: \LiveTVVideoPlayerCoordinator.start)
|
||||
|
||||
@Root
|
||||
var start = makeStart
|
||||
|
||||
let viewModel: VideoPlayerViewModel
|
||||
|
||||
init(viewModel: VideoPlayerViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeStart() -> some View {
|
||||
if Defaults[.Experimental.liveTVNativePlayer] {
|
||||
LiveTVNativePlayerView(viewModel: viewModel)
|
||||
.navigationBarHidden(true)
|
||||
.ignoresSafeArea()
|
||||
} else {
|
||||
LiveTVPlayerView(viewModel: viewModel)
|
||||
.navigationBarHidden(true)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -236,6 +236,7 @@ public extension BaseItemDto {
|
|||
case boxset = "BoxSet"
|
||||
case collectionFolder = "CollectionFolder"
|
||||
case folder = "Folder"
|
||||
case liveTV = "LiveTV"
|
||||
|
||||
case unknown
|
||||
|
||||
|
@ -247,6 +248,21 @@ public extension BaseItemDto {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public init?(rawValue: String) {
|
||||
let lowerCase = rawValue.lowercased()
|
||||
switch lowerCase {
|
||||
case "movie": self = .movie
|
||||
case "season": self = .season
|
||||
case "episode": self = .episode
|
||||
case "series": self = .series
|
||||
case "boxset": self = .boxset
|
||||
case "collectionfolder": self = .collectionFolder
|
||||
case "folder": self = .folder
|
||||
case "livetv": self = .liveTV
|
||||
default: self = .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var itemType: ItemType {
|
||||
|
@ -258,7 +274,7 @@ public extension BaseItemDto {
|
|||
|
||||
func portraitHeaderViewURL(maxWidth: Int) -> URL {
|
||||
switch itemType {
|
||||
case .movie, .season, .series, .boxset, .collectionFolder, .folder:
|
||||
case .movie, .season, .series, .boxset, .collectionFolder, .folder, .liveTV:
|
||||
return getPrimaryImage(maxWidth: maxWidth)
|
||||
case .episode:
|
||||
return getSeriesPrimaryImage(maxWidth: maxWidth)
|
||||
|
|
|
@ -188,6 +188,10 @@ extension UIScreen {
|
|||
let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width
|
||||
let itemSize = width * height
|
||||
|
||||
return Int(screenSize / itemSize)
|
||||
#if os(tvOS)
|
||||
return Int(screenSize / itemSize) * 2
|
||||
#else
|
||||
return Int(screenSize / itemSize)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ struct LiveTVChannelRowCell: Hashable {
|
|||
struct LiveTVChannelProgram: Hashable {
|
||||
let id = UUID()
|
||||
let channel: BaseItemDto
|
||||
let program: BaseItemDto?
|
||||
let currentProgram: BaseItemDto?
|
||||
let programs: [BaseItemDto]
|
||||
}
|
||||
|
||||
final class LiveTVChannelsViewModel: ViewModel {
|
||||
|
@ -151,7 +152,7 @@ final class LiveTVChannelsViewModel: ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg))
|
||||
channelPrograms.append(LiveTVChannelProgram(channel: channel, currentProgram: currentPrg, programs: prgs))
|
||||
}
|
||||
return channelPrograms
|
||||
}
|
||||
|
|
|
@ -54,8 +54,8 @@ final class MovieLibrariesViewModel: ViewModel {
|
|||
}
|
||||
self.rows = self.calculateRows()
|
||||
if self.libraries.count == 1, let library = self.libraries.first {
|
||||
// show library
|
||||
self.router?.route(to: \.library, library)
|
||||
// make this library the root of this stack
|
||||
self.router?.coordinator.root(\.rootLibrary, library)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -54,8 +54,8 @@ final class TVLibrariesViewModel: ViewModel {
|
|||
}
|
||||
self.rows = self.calculateRows()
|
||||
if self.libraries.count == 1, let library = self.libraries.first {
|
||||
// show library
|
||||
self.router?.route(to: \.library, library)
|
||||
// make this library the root of this stack
|
||||
self.router?.coordinator.root(\.rootLibrary, library)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import Defaults
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryListView: View {
|
||||
|
@ -21,7 +23,13 @@ struct LibraryListView: View {
|
|||
@Default(.Experimental.liveTVAlphaEnabled)
|
||||
var liveTVAlphaEnabled
|
||||
|
||||
let supportedCollectionTypes = ["movies", "tvshows", "boxsets", "livetv", "other"]
|
||||
var supportedCollectionTypes: [BaseItemDto.ItemType] {
|
||||
if liveTVAlphaEnabled {
|
||||
return [.movie, .season, .series, .liveTV, .boxset, .unknown]
|
||||
} else {
|
||||
return [.movie, .season, .series, .boxset, .unknown]
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
|
@ -30,58 +38,37 @@ struct LibraryListView: View {
|
|||
|
||||
ForEach(viewModel.libraries.filter { [self] library in
|
||||
let collectionType = library.collectionType ?? "other"
|
||||
return self.supportedCollectionTypes.contains(collectionType)
|
||||
let itemType = BaseItemDto.ItemType(rawValue: collectionType) ?? .unknown
|
||||
return self.supportedCollectionTypes.contains(itemType)
|
||||
}, id: \.id) { library in
|
||||
if library.collectionType == "livetv" {
|
||||
if liveTVAlphaEnabled {
|
||||
Button {
|
||||
self.mainCoordinator.root(\.liveTV)
|
||||
}
|
||||
label: {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
Text(library.name ?? "")
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
Spacer()
|
||||
}.padding(32)
|
||||
}
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
.frame(height: 100)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
Button {
|
||||
let itemType = BaseItemDto.ItemType(rawValue: library.collectionType ?? "other") ?? .unknown
|
||||
if itemType == .liveTV {
|
||||
self.mainCoordinator.root(\.liveTV)
|
||||
} else {
|
||||
self.libraryListRouter.route(to: \.library,
|
||||
(viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? ""))
|
||||
}
|
||||
label: {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
Text(library.name ?? "")
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
Spacer()
|
||||
}.padding(32)
|
||||
}
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
.frame(height: 100)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
label: {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
Text(library.name ?? "")
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
Spacer()
|
||||
}.padding(32)
|
||||
}
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
.frame(height: 100)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
|
|
|
@ -27,7 +27,7 @@ struct LibraryView: View {
|
|||
var isShowingFilterView = false
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
if viewModel.rows.isEmpty && viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
} else if !viewModel.rows.isEmpty {
|
||||
CollectionView(rows: viewModel.rows) { _, _ in
|
||||
|
|
|
@ -18,14 +18,25 @@ struct LiveTVChannelItemElement: View {
|
|||
private var isFocused: Bool = false
|
||||
|
||||
var channel: BaseItemDto
|
||||
var program: BaseItemDto?
|
||||
var startString = " "
|
||||
var endString = " "
|
||||
var progressPercent = Double(0)
|
||||
var currentProgram: BaseItemDto?
|
||||
var currentProgramText: LiveTVChannelViewProgram
|
||||
var nextProgramsText: [LiveTVChannelViewProgram]
|
||||
var onSelect: (@escaping (Bool) -> Void) -> Void
|
||||
|
||||
var progressPercent: Double {
|
||||
if let currentProgram = currentProgram {
|
||||
let progressPercent = currentProgram.getLiveProgressPercentage()
|
||||
if progressPercent > 1.0 {
|
||||
return 1.0
|
||||
} else {
|
||||
return progressPercent
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private var detailText: String {
|
||||
guard let program = program else {
|
||||
guard let program = currentProgram else {
|
||||
return ""
|
||||
}
|
||||
var text = ""
|
||||
|
@ -60,61 +71,62 @@ struct LiveTVChannelItemElement: View {
|
|||
}.frame(alignment: .top)
|
||||
Spacer()
|
||||
}
|
||||
VStack {
|
||||
ImageView(channel.getPrimaryImage(maxWidth: 128))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 128, alignment: .center)
|
||||
.padding(.init(top: 8, leading: 0, bottom: 0, trailing: 0))
|
||||
Text(channel.name ?? "?")
|
||||
.font(.footnote)
|
||||
.lineLimit(1)
|
||||
.frame(alignment: .center)
|
||||
Text(program?.name ?? L10n.notAvailableSlash)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.green)
|
||||
Text(detailText)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.green)
|
||||
Spacer()
|
||||
HStack(alignment: .bottom) {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Text(startString)
|
||||
.font(.footnote)
|
||||
.lineLimit(1)
|
||||
.frame(alignment: .leading)
|
||||
|
||||
Spacer()
|
||||
GeometryReader { gp in
|
||||
VStack {
|
||||
ImageView(channel.getPrimaryImage(maxWidth: 192))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 192, alignment: .center)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.padding(.init(top: 16, leading: 8, bottom: gp.size.height / 2, trailing: 0))
|
||||
VStack {
|
||||
Text(channel.name ?? "?")
|
||||
.font(.footnote)
|
||||
.lineLimit(1)
|
||||
.frame(alignment: .center)
|
||||
.foregroundColor(Color.jellyfinPurple)
|
||||
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
|
||||
|
||||
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.jellyfinPurple)
|
||||
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
|
||||
}
|
||||
.frame(alignment: .bottom)
|
||||
}
|
||||
programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title,
|
||||
color: Color("TextHighlightColor"), font: Font.system(size: 20, weight: .bold, design: .default))
|
||||
if !nextProgramsText.isEmpty,
|
||||
let nextItem = nextProgramsText[0]
|
||||
{
|
||||
programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray,
|
||||
font: Font.system(size: 20, design: .default))
|
||||
}
|
||||
if nextProgramsText.count > 1,
|
||||
let nextItem2 = nextProgramsText[1]
|
||||
{
|
||||
programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray,
|
||||
font: Font.system(size: 20, design: .default))
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.padding(.init(top: gp.size.height / 2, leading: 16, bottom: 56, trailing: 16))
|
||||
.opacity(loading ? 0.5 : 1.0)
|
||||
}
|
||||
.padding()
|
||||
.opacity(loading ? 0.5 : 1.0)
|
||||
|
||||
if loading {
|
||||
ProgressView()
|
||||
}
|
||||
|
||||
VStack {
|
||||
GeometryReader { gp in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.gray)
|
||||
.opacity(0.4)
|
||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 8, maxHeight: 8)
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.jellyfinPurple)
|
||||
.frame(width: CGFloat(progressPercent * gp.size.width), height: 8)
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||
.padding(.init(top: 0, leading: 16, bottom: 32, trailing: 16))
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4))
|
||||
|
@ -133,4 +145,20 @@ struct LiveTVChannelItemElement: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func programLabel(timeText: String, titleText: String, color: Color, font: Font) -> some View {
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
Text(timeText)
|
||||
.font(font)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(color)
|
||||
.frame(width: 54, alignment: .leading)
|
||||
Text(titleText)
|
||||
.font(font)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(color)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import JellyfinAPI
|
|||
import SwiftUI
|
||||
import SwiftUICollection
|
||||
|
||||
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
|
||||
|
||||
struct LiveTVChannelsView: View {
|
||||
@EnvironmentObject
|
||||
var router: LiveTVChannelsCoordinator.Router
|
||||
|
@ -53,23 +55,30 @@ struct LiveTVChannelsView: View {
|
|||
func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
|
||||
let item = cell.item
|
||||
let channel = item.channel
|
||||
if channel.type != "Folder" {
|
||||
let progressPercent = item.program?.getLiveProgressPercentage() ?? 0
|
||||
LiveTVChannelItemElement(channel: channel,
|
||||
program: item.program,
|
||||
startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ",
|
||||
endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ",
|
||||
progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent,
|
||||
onSelect: { loadingAction in
|
||||
loadingAction(true)
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
|
||||
self.router.route(to: \.videoPlayer, playerViewModel)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
loadingAction(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
let currentProgramDisplayText = item.currentProgram?
|
||||
.programDisplayText(timeFormatter: viewModel.timeFormatter) ?? LiveTVChannelViewProgram(timeDisplay: "", title: "")
|
||||
let nextItems = item.programs.filter { program in
|
||||
guard let start = program.startDate else {
|
||||
return false
|
||||
}
|
||||
guard let currentStart = item.currentProgram?.startDate else {
|
||||
return false
|
||||
}
|
||||
return start > currentStart
|
||||
}
|
||||
LiveTVChannelItemElement(channel: channel,
|
||||
currentProgram: item.currentProgram,
|
||||
currentProgramText: currentProgramDisplayText,
|
||||
nextProgramsText: nextProgramsDisplayText(nextItems: nextItems, timeFormatter: viewModel.timeFormatter),
|
||||
onSelect: { loadingAction in
|
||||
loadingAction(true)
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
|
||||
self.router.route(to: \.videoPlayer, playerViewModel)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
loadingAction(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func createGridLayout() -> NSCollectionLayoutSection {
|
||||
|
@ -100,4 +109,43 @@ struct LiveTVChannelsView: View {
|
|||
|
||||
return section
|
||||
}
|
||||
|
||||
private func nextProgramsDisplayText(nextItems: [BaseItemDto], timeFormatter: DateFormatter) -> [LiveTVChannelViewProgram] {
|
||||
var programsDisplayText: [LiveTVChannelViewProgram] = []
|
||||
for item in nextItems {
|
||||
programsDisplayText.append(item.programDisplayText(timeFormatter: timeFormatter))
|
||||
}
|
||||
return programsDisplayText
|
||||
}
|
||||
}
|
||||
|
||||
private extension BaseItemDto {
|
||||
func programDisplayText(timeFormatter: DateFormatter) -> LiveTVChannelViewProgram {
|
||||
var timeText = ""
|
||||
if let start = self.startDate {
|
||||
timeText.append(timeFormatter.string(from: start) + " ")
|
||||
}
|
||||
var displayText = ""
|
||||
if let season = self.parentIndexNumber,
|
||||
let episode = self.indexNumber
|
||||
{
|
||||
displayText.append("\(season)x\(episode) ")
|
||||
} else if let episode = self.indexNumber {
|
||||
displayText.append("\(episode) ")
|
||||
}
|
||||
if let name = self.name {
|
||||
displayText.append("\(name) ")
|
||||
}
|
||||
if let title = self.episodeTitle {
|
||||
displayText.append("\(title) ")
|
||||
}
|
||||
if let year = self.productionYear {
|
||||
displayText.append("\(year) ")
|
||||
}
|
||||
if let rating = self.officialRating {
|
||||
displayText.append("\(rating)")
|
||||
}
|
||||
|
||||
return LiveTVChannelViewProgram(timeDisplay: timeText, title: displayText)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -250,13 +250,26 @@
|
|||
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; };
|
||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; };
|
||||
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
|
||||
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */; };
|
||||
C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
|
||||
C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */; };
|
||||
C409CE9C284EA6EA00CABC12 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C409CE9B284EA6EA00CABC12 /* SwiftUICollection */; };
|
||||
C409CE9E285044C800CABC12 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C409CE9D285044C800CABC12 /* SwiftUICollection */; };
|
||||
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; };
|
||||
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; };
|
||||
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
|
||||
C4464953281616AE00DDB461 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
|
||||
C453497F279A2DA50045F1E2 /* LiveTVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C453497E279A2DA50045F1E2 /* LiveTVPlayerViewController.swift */; };
|
||||
C4534981279A3F140045F1E2 /* tvOSLiveTVOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */; };
|
||||
C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */; };
|
||||
C4534985279A40C60045F1E2 /* LiveTVVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534984279A40C50045F1E2 /* LiveTVVideoPlayerView.swift */; };
|
||||
C45640D0281A43EF007096DE /* LiveTVNativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45640CF281A43EF007096DE /* LiveTVNativePlayerViewController.swift */; };
|
||||
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */; };
|
||||
C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
|
||||
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942C827F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift */; };
|
||||
C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942CA27F6984100C54FE7 /* LiveTVPlayerViewController.swift */; };
|
||||
C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942CC27F6994A00C54FE7 /* LiveTVPlayerView.swift */; };
|
||||
C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.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 */; };
|
||||
|
@ -276,9 +289,10 @@
|
|||
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
||||
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
||||
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; };
|
||||
C4D0CE4B2848570700345D11 /* ASCollectionView in Frameworks */ = {isa = PBXBuildFile; productRef = C4D0CE4A2848570700345D11 /* ASCollectionView */; };
|
||||
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
||||
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
|
||||
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
|
||||
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */; };
|
||||
E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */; };
|
||||
E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; };
|
||||
E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; };
|
||||
|
@ -425,8 +439,6 @@
|
|||
E1A2C15D279A7D9F005EC829 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15B279A7D9F005EC829 /* AppIcon.swift */; };
|
||||
E1A2C15E279A7D9F005EC829 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15B279A7D9F005EC829 /* AppIcon.swift */; };
|
||||
E1A2C160279A7DCA005EC829 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15F279A7DCA005EC829 /* AboutView.swift */; };
|
||||
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A99998271A3429008E78C0 /* SwiftUICollection */; };
|
||||
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = E1A9999A271A343C008E78C0 /* SwiftUICollection */; };
|
||||
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButtonView.swift */; };
|
||||
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
||||
E1AA33202782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
||||
|
@ -732,6 +744,8 @@
|
|||
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
|
||||
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
|
||||
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
|
||||
C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = "<group>"; };
|
||||
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemWideElement.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>"; };
|
||||
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = "<group>"; };
|
||||
|
@ -739,6 +753,11 @@
|
|||
C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVOverlay.swift; sourceTree = "<group>"; };
|
||||
C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||
C4534984279A40C50045F1E2 /* LiveTVVideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVVideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
C45640CF281A43EF007096DE /* LiveTVNativePlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVNativePlayerViewController.swift; sourceTree = "<group>"; };
|
||||
C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVCoordinator.swift; sourceTree = "<group>"; };
|
||||
C45942C827F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLiveTVVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||
C45942CA27F6984100C54FE7 /* LiveTVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
C45942CC27F6994A00C54FE7 /* LiveTVPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVPlayerView.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>"; };
|
||||
C4B9B91327E1921B0063535C /* LiveTVNativeVideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVNativeVideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
|
@ -757,6 +776,7 @@
|
|||
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>"; };
|
||||
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = "<group>"; };
|
||||
E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfoExtensions.swift; sourceTree = "<group>"; };
|
||||
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||
|
@ -894,6 +914,7 @@
|
|||
files = (
|
||||
62666E1727E501CC00EC0ECD /* CFNetwork.framework in Frameworks */,
|
||||
E11D83AF278FA998006E9776 /* NukeUI in Frameworks */,
|
||||
C409CE9C284EA6EA00CABC12 /* SwiftUICollection in Frameworks */,
|
||||
62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */,
|
||||
62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */,
|
||||
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */,
|
||||
|
@ -911,7 +932,6 @@
|
|||
62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */,
|
||||
62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */,
|
||||
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
||||
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */,
|
||||
62666E1F27E501DF00EC0ECD /* CoreText.framework in Frameworks */,
|
||||
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
||||
62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */,
|
||||
|
@ -928,6 +948,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C4D0CE4B2848570700345D11 /* ASCollectionView in Frameworks */,
|
||||
62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */,
|
||||
62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */,
|
||||
E13DD3D327168E65009D4DAF /* Defaults in Frameworks */,
|
||||
|
@ -944,7 +965,7 @@
|
|||
62666E1027E501B400EC0ECD /* VideoToolbox.framework in Frameworks */,
|
||||
62666E3C27E503F200EC0ECD /* GoogleCastSDK.xcframework in Frameworks */,
|
||||
62666E0C27E501A500EC0ECD /* OpenGLES.framework in Frameworks */,
|
||||
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */,
|
||||
C409CE9E285044C800CABC12 /* SwiftUICollection in Frameworks */,
|
||||
62666E0127E5016900EC0ECD /* CoreFoundation.framework in Frameworks */,
|
||||
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */,
|
||||
62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */,
|
||||
|
@ -1475,6 +1496,7 @@
|
|||
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
|
||||
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */,
|
||||
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */,
|
||||
C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */,
|
||||
E193D5412719404B00900D82 /* MainCoordinator */,
|
||||
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
||||
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
||||
|
@ -1632,6 +1654,9 @@
|
|||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
|
||||
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
||||
C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */,
|
||||
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */,
|
||||
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */,
|
||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
||||
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
||||
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
||||
|
@ -1730,10 +1755,13 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */,
|
||||
C45640CF281A43EF007096DE /* LiveTVNativePlayerViewController.swift */,
|
||||
E1002B692793E12E00E47059 /* Overlays */,
|
||||
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
|
||||
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */,
|
||||
C45942CC27F6994A00C54FE7 /* LiveTVPlayerView.swift */,
|
||||
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */,
|
||||
C45942CA27F6984100C54FE7 /* LiveTVPlayerViewController.swift */,
|
||||
);
|
||||
path = VideoPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1810,6 +1838,7 @@
|
|||
E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C45942C827F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift */,
|
||||
6220D0C526D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift */,
|
||||
C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */,
|
||||
E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */,
|
||||
|
@ -1899,12 +1928,12 @@
|
|||
E13DD3CC27164CA7009D4DAF /* CoreStore */,
|
||||
E12186DD2718F1C50010884C /* Defaults */,
|
||||
E1218C9D271A2CD600EA0737 /* CombineExt */,
|
||||
E1A9999A271A343C008E78C0 /* SwiftUICollection */,
|
||||
E178857C278037FD0094FBCF /* JellyfinAPI */,
|
||||
E1AE8E7D2789136D00FBDDAA /* Nuke */,
|
||||
E11D83AE278FA998006E9776 /* NukeUI */,
|
||||
E1002B6A2793E36600E47059 /* Algorithms */,
|
||||
E1347DB5279E3CA500BC6161 /* Puppy */,
|
||||
C409CE9B284EA6EA00CABC12 /* SwiftUICollection */,
|
||||
);
|
||||
productName = "JellyfinPlayer tvOS";
|
||||
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
|
||||
|
@ -1936,7 +1965,6 @@
|
|||
E13DD3D227168E65009D4DAF /* Defaults */,
|
||||
E1B6DCE7271A23780015B715 /* CombineExt */,
|
||||
E1B6DCE9271A23880015B715 /* SwiftyJSON */,
|
||||
E1A99998271A3429008E78C0 /* SwiftUICollection */,
|
||||
E10EAA44277BB646000269ED /* JellyfinAPI */,
|
||||
E10EAA4C277BB716000269ED /* Sliders */,
|
||||
E1AE8E7B2789135A00FBDDAA /* Nuke */,
|
||||
|
@ -1944,6 +1972,8 @@
|
|||
E1002B672793CFBA00E47059 /* Algorithms */,
|
||||
62666E3827E502CE00EC0ECD /* SwizzleSwift */,
|
||||
E1101176281B1E8A006A3584 /* Puppy */,
|
||||
C4D0CE4A2848570700345D11 /* ASCollectionView */,
|
||||
C409CE9D285044C800CABC12 /* SwiftUICollection */,
|
||||
);
|
||||
productName = JellyfinPlayer;
|
||||
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
|
||||
|
@ -2030,7 +2060,6 @@
|
|||
E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */,
|
||||
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */,
|
||||
E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||
C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
|
||||
E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
|
||||
E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */,
|
||||
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
|
@ -2038,6 +2067,8 @@
|
|||
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
|
||||
62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */,
|
||||
E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */,
|
||||
C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */,
|
||||
C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
|
||||
);
|
||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -2069,6 +2100,7 @@
|
|||
53913C0526D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||
53913BFF26D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||
53913C0E26D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||
C4464953281616AE00DDB461 /* Assets.xcassets in Resources */,
|
||||
53913BF026D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||
53913C0826D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||
53913C1126D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||
|
@ -2243,6 +2275,7 @@
|
|||
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||
E193D54B271941D300900D82 /* ServerListView.swift in Sources */,
|
||||
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
|
||||
C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */,
|
||||
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||
6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */,
|
||||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
|
@ -2268,7 +2301,6 @@
|
|||
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||
E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */,
|
||||
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */,
|
||||
E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
|
||||
E1A2C15E279A7D9F005EC829 /* AppIcon.swift in Sources */,
|
||||
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
|
||||
|
@ -2378,9 +2410,11 @@
|
|||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
|
||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||
C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */,
|
||||
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */,
|
||||
C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */,
|
||||
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
||||
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
||||
|
@ -2399,9 +2433,11 @@
|
|||
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
|
||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||
E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */,
|
||||
C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */,
|
||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||
E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */,
|
||||
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
||||
C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */,
|
||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
||||
|
@ -2413,12 +2449,14 @@
|
|||
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */,
|
||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
|
||||
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */,
|
||||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
||||
E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */,
|
||||
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */,
|
||||
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */,
|
||||
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */,
|
||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
||||
C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */,
|
||||
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
||||
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
||||
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
|
||||
|
@ -2469,12 +2507,14 @@
|
|||
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
||||
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
||||
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */,
|
||||
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */,
|
||||
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
||||
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */,
|
||||
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
|
||||
E1A2C154279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
|
||||
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
|
||||
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */,
|
||||
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
|
||||
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
|
||||
|
@ -2486,7 +2526,9 @@
|
|||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */,
|
||||
E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
||||
C45640D0281A43EF007096DE /* LiveTVNativePlayerViewController.swift in Sources */,
|
||||
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||
|
@ -3083,14 +3125,22 @@
|
|||
minimumVersion = 2.0.2;
|
||||
};
|
||||
};
|
||||
C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */ = {
|
||||
C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/ABJC/SwiftUICollection";
|
||||
repositoryURL = "https://github.com/defagos/SwiftUICollection";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apptekstudios/ASCollectionView";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-algorithms.git";
|
||||
|
@ -3227,6 +3277,21 @@
|
|||
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
||||
productName = Stinsen;
|
||||
};
|
||||
C409CE9B284EA6EA00CABC12 /* SwiftUICollection */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
||||
productName = SwiftUICollection;
|
||||
};
|
||||
C409CE9D285044C800CABC12 /* SwiftUICollection */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
||||
productName = SwiftUICollection;
|
||||
};
|
||||
C4D0CE4A2848570700345D11 /* ASCollectionView */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */;
|
||||
productName = ASCollectionView;
|
||||
};
|
||||
E1002B672793CFBA00E47059 /* Algorithms */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
|
||||
|
@ -3322,16 +3387,6 @@
|
|||
package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */;
|
||||
productName = JellyfinAPI;
|
||||
};
|
||||
E1A99998271A3429008E78C0 /* SwiftUICollection */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
||||
productName = SwiftUICollection;
|
||||
};
|
||||
E1A9999A271A343C008E78C0 /* SwiftUICollection */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
||||
productName = SwiftUICollection;
|
||||
};
|
||||
E1AE8E7B2789135A00FBDDAA /* Nuke */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
|
|
|
@ -18,6 +18,15 @@
|
|||
"version" : "0.6.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ascollectionview",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apptekstudios/ASCollectionView",
|
||||
"state" : {
|
||||
"revision" : "4288744ba484c1062c109c0f28d72b629d321d55",
|
||||
"version" : "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "combineext",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -45,6 +54,15 @@
|
|||
"version" : "6.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "differencekit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ra1028/DifferenceKit",
|
||||
"state" : {
|
||||
"revision" : "62745d7780deef4a023a792a1f8f763ec7bf9705",
|
||||
"version" : "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gifu",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -156,10 +174,10 @@
|
|||
{
|
||||
"identity" : "swiftuicollection",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ABJC/SwiftUICollection",
|
||||
"location" : "https://github.com/defagos/SwiftUICollection",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "e27149382ce8ec21995069c8aab7ca83d61a3120"
|
||||
"revision" : "5b9f14eb3ec5d48cec8b3e4462dcc554d4bff2a8"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.100",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.100",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.250",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.250",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "light"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -6,7 +6,9 @@
|
|||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
|
@ -16,7 +18,16 @@ struct LibraryListView: View {
|
|||
@StateObject
|
||||
var viewModel = LibraryListViewModel()
|
||||
|
||||
let supportedCollectionTypes = ["movies", "tvshows", "boxsets", "other"]
|
||||
@Default(.Experimental.liveTVAlphaEnabled)
|
||||
var liveTVAlphaEnabled
|
||||
|
||||
var supportedCollectionTypes: [BaseItemDto.ItemType] {
|
||||
if liveTVAlphaEnabled {
|
||||
return [.movie, .season, .series, .liveTV, .boxset, .unknown]
|
||||
} else {
|
||||
return [.movie, .season, .series, .boxset, .unknown]
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
|
@ -46,12 +57,18 @@ struct LibraryListView: View {
|
|||
if !viewModel.isLoading {
|
||||
ForEach(viewModel.libraries.filter { [self] library in
|
||||
let collectionType = library.collectionType ?? "other"
|
||||
return self.supportedCollectionTypes.contains(collectionType)
|
||||
let itemType = BaseItemDto.ItemType(rawValue: collectionType) ?? .unknown
|
||||
return self.supportedCollectionTypes.contains(itemType)
|
||||
}, id: \.id) { library in
|
||||
Button {
|
||||
libraryListRouter.route(to: \.library,
|
||||
(viewModel: LibraryViewModel(parentID: library.id),
|
||||
title: library.name ?? ""))
|
||||
let itemType = BaseItemDto.ItemType(rawValue: library.collectionType ?? "other") ?? .unknown
|
||||
if itemType == .liveTV {
|
||||
libraryListRouter.route(to: \.liveTV)
|
||||
} else {
|
||||
libraryListRouter.route(to: \.library,
|
||||
(viewModel: LibraryViewModel(parentID: library.id),
|
||||
title: library.name ?? ""))
|
||||
}
|
||||
} label: {
|
||||
ZStack {
|
||||
ImageView(library.getPrimaryImage(maxWidth: 500), blurHash: library.getPrimaryImageBlurHash())
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
//
|
||||
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct LiveTVChannelItemElement: View {
|
||||
@FocusState
|
||||
private var focused: Bool
|
||||
@State
|
||||
private var loading: Bool = false
|
||||
@State
|
||||
private var isFocused: Bool = false
|
||||
|
||||
var channel: BaseItemDto
|
||||
var program: BaseItemDto?
|
||||
var startString = " "
|
||||
var endString = " "
|
||||
var progressPercent = Double(0)
|
||||
var onSelect: (@escaping (Bool) -> Void) -> Void
|
||||
|
||||
private var detailText: String {
|
||||
guard let program = program else {
|
||||
return ""
|
||||
}
|
||||
var text = ""
|
||||
if let season = program.parentIndexNumber,
|
||||
let episode = program.indexNumber
|
||||
{
|
||||
text.append("\(season)x\(episode) ")
|
||||
} else if let episode = program.indexNumber {
|
||||
text.append("\(episode) ")
|
||||
}
|
||||
if let title = program.episodeTitle {
|
||||
text.append("\(title) ")
|
||||
}
|
||||
if let year = program.productionYear {
|
||||
text.append("\(year) ")
|
||||
}
|
||||
if let rating = program.officialRating {
|
||||
text.append("\(rating)")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(channel.number ?? "")
|
||||
.font(.footnote)
|
||||
.frame(alignment: .leading)
|
||||
.padding()
|
||||
Spacer()
|
||||
}.frame(alignment: .top)
|
||||
Spacer()
|
||||
}
|
||||
VStack {
|
||||
ImageView(channel.getPrimaryImage(maxWidth: 128))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 128, alignment: .center)
|
||||
.padding(.init(top: 8, leading: 0, bottom: 0, trailing: 0))
|
||||
Text(channel.name ?? "?")
|
||||
.font(.footnote)
|
||||
.lineLimit(1)
|
||||
.frame(alignment: .center)
|
||||
Text(program?.name ?? L10n.notAvailableSlash)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.green)
|
||||
Text(detailText)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.green)
|
||||
Spacer()
|
||||
HStack(alignment: .bottom) {
|
||||
VStack {
|
||||
Spacer()
|
||||
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.jellyfinPurple)
|
||||
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
|
||||
}
|
||||
.frame(alignment: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.opacity(loading ? 0.5 : 1.0)
|
||||
|
||||
if loading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 0)
|
||||
.stroke(Color.blue, lineWidth: 0))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct LiveTVChannelItemWideElement: View {
|
||||
@FocusState
|
||||
private var focused: Bool
|
||||
@State
|
||||
private var loading: Bool = false
|
||||
@State
|
||||
private var isFocused: Bool = false
|
||||
|
||||
var channel: BaseItemDto
|
||||
var currentProgram: BaseItemDto?
|
||||
var currentProgramText: LiveTVChannelViewProgram
|
||||
var nextProgramsText: [LiveTVChannelViewProgram]
|
||||
var onSelect: (@escaping (Bool) -> Void) -> Void
|
||||
|
||||
var progressPercent: Double {
|
||||
if let currentProgram = currentProgram {
|
||||
let progressPercent = currentProgram.getLiveProgressPercentage()
|
||||
if progressPercent > 1.0 {
|
||||
return 1.0
|
||||
} else {
|
||||
return progressPercent
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private var detailText: String {
|
||||
guard let program = currentProgram else {
|
||||
return ""
|
||||
}
|
||||
var text = ""
|
||||
if let season = program.parentIndexNumber,
|
||||
let episode = program.indexNumber
|
||||
{
|
||||
text.append("\(season)x\(episode) ")
|
||||
} else if let episode = program.indexNumber {
|
||||
text.append("\(episode) ")
|
||||
}
|
||||
if let title = program.episodeTitle {
|
||||
text.append("\(title) ")
|
||||
}
|
||||
if let year = program.productionYear {
|
||||
text.append("\(year) ")
|
||||
}
|
||||
if let rating = program.officialRating {
|
||||
text.append("\(rating)")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ZStack {
|
||||
HStack {
|
||||
ZStack(alignment: .center) {
|
||||
ImageView(channel.getPrimaryImage(maxWidth: 128))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
|
||||
VStack(alignment: .center) {
|
||||
Spacer()
|
||||
.frame(maxHeight: .infinity)
|
||||
GeometryReader { gp in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.fill(Color.gray)
|
||||
.opacity(0.4)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 6, maxHeight: 6)
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.jellyfinPurple)
|
||||
.frame(width: CGFloat(progressPercent * gp.size.width), height: 6)
|
||||
}
|
||||
}
|
||||
.frame(height: 6, alignment: .center)
|
||||
.padding(.init(top: 0, leading: 4, bottom: 0, trailing: 4))
|
||||
}
|
||||
if loading {
|
||||
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.aspectRatio(1.0, contentMode: .fit)
|
||||
VStack(alignment: .leading) {
|
||||
let channelNumber = channel.number != nil ? "\(channel.number ?? "") " : ""
|
||||
let channelName = "\(channelNumber)\(channel.name ?? "?")"
|
||||
Text(channelName)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(Color.jellyfinPurple)
|
||||
.frame(alignment: .leading)
|
||||
.padding(.init(top: 0, leading: 0, bottom: 4, trailing: 0))
|
||||
programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title,
|
||||
color: Color("TextHighlightColor"))
|
||||
if !nextProgramsText.isEmpty,
|
||||
let nextItem = nextProgramsText[0]
|
||||
{
|
||||
programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray)
|
||||
}
|
||||
if nextProgramsText.count > 1,
|
||||
let nextItem2 = nextProgramsText[1]
|
||||
{
|
||||
programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(alignment: .leading)
|
||||
.padding()
|
||||
.opacity(loading ? 0.5 : 1.0)
|
||||
}
|
||||
.background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color("BackgroundSecondaryColor")))
|
||||
.frame(height: 128)
|
||||
.onTapGesture {
|
||||
onSelect { loadingState in
|
||||
loading = loadingState
|
||||
}
|
||||
}
|
||||
}
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color("BackgroundColor"))
|
||||
.shadow(color: Color("ShadowColor"), radius: 4, x: 0, y: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func programLabel(timeText: String, titleText: String, color: Color) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
Text(timeText)
|
||||
.font(.footnote)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(color)
|
||||
.frame(width: 38, alignment: .leading)
|
||||
Text(titleText)
|
||||
.font(.footnote)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(color)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import ASCollectionView
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
import SwiftUICollection
|
||||
|
||||
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
|
||||
|
||||
struct LiveTVChannelsView: View {
|
||||
@EnvironmentObject
|
||||
var router: LiveTVCoordinator.Router
|
||||
@StateObject
|
||||
var viewModel = LiveTVChannelsViewModel()
|
||||
@State
|
||||
private var isPortrait = false
|
||||
private var columns: Int {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
return 2
|
||||
} else {
|
||||
if isPortrait {
|
||||
return 1
|
||||
} else {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
} else if !viewModel.channelPrograms.isEmpty {
|
||||
ASCollectionView(data: viewModel.channelPrograms, dataID: \.self) { channelProgram, _ in
|
||||
makeCellView(channelProgram)
|
||||
}
|
||||
.layout {
|
||||
.grid(layoutMode: .fixedNumberOfColumns(columns),
|
||||
itemSpacing: 16,
|
||||
lineSpacing: 4,
|
||||
itemSize: .absolute(144))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
viewModel.startScheduleCheckTimer()
|
||||
self.checkOrientation()
|
||||
}
|
||||
.onDisappear {
|
||||
viewModel.stopScheduleCheckTimer()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||
self.checkOrientation()
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
Text("No results.")
|
||||
Button {
|
||||
viewModel.getChannels()
|
||||
} label: {
|
||||
Text("Reload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeCellView(_ channelProgram: LiveTVChannelProgram) -> some View {
|
||||
let channel = channelProgram.channel
|
||||
let currentProgramDisplayText = channelProgram.currentProgram?
|
||||
.programDisplayText(timeFormatter: viewModel.timeFormatter) ?? LiveTVChannelViewProgram(timeDisplay: "", title: "")
|
||||
let nextItems = channelProgram.programs.filter { program in
|
||||
guard let start = program.startDate else {
|
||||
return false
|
||||
}
|
||||
guard let currentStart = channelProgram.currentProgram?.startDate else {
|
||||
return false
|
||||
}
|
||||
return start > currentStart
|
||||
}
|
||||
LiveTVChannelItemWideElement(channel: channel,
|
||||
currentProgram: channelProgram.currentProgram,
|
||||
currentProgramText: currentProgramDisplayText,
|
||||
nextProgramsText: nextProgramsDisplayText(nextItems: nextItems,
|
||||
timeFormatter: viewModel.timeFormatter),
|
||||
onSelect: { loadingAction in
|
||||
loadingAction(true)
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
|
||||
self.router.route(to: \.videoPlayer, playerViewModel)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
loadingAction(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func checkOrientation() {
|
||||
let scenes = UIApplication.shared.connectedScenes
|
||||
let windowScene = scenes.first as? UIWindowScene
|
||||
guard let scene = windowScene else { return }
|
||||
self.isPortrait = scene.interfaceOrientation.isPortrait
|
||||
}
|
||||
|
||||
private func nextProgramsDisplayText(nextItems: [BaseItemDto], timeFormatter: DateFormatter) -> [LiveTVChannelViewProgram] {
|
||||
var programsDisplayText: [LiveTVChannelViewProgram] = []
|
||||
for item in nextItems {
|
||||
programsDisplayText.append(item.programDisplayText(timeFormatter: timeFormatter))
|
||||
}
|
||||
return programsDisplayText
|
||||
}
|
||||
}
|
||||
|
||||
private extension BaseItemDto {
|
||||
func programDisplayText(timeFormatter: DateFormatter) -> LiveTVChannelViewProgram {
|
||||
var timeText = ""
|
||||
if let start = self.startDate {
|
||||
timeText.append(timeFormatter.string(from: start) + " ")
|
||||
}
|
||||
var displayText = ""
|
||||
if let season = self.parentIndexNumber,
|
||||
let episode = self.indexNumber
|
||||
{
|
||||
displayText.append("\(season)x\(episode) ")
|
||||
} else if let episode = self.indexNumber {
|
||||
displayText.append("\(episode) ")
|
||||
}
|
||||
if let name = self.name {
|
||||
displayText.append("\(name) ")
|
||||
}
|
||||
if let title = self.episodeTitle {
|
||||
displayText.append("\(title) ")
|
||||
}
|
||||
if let year = self.productionYear {
|
||||
displayText.append("\(year) ")
|
||||
}
|
||||
if let rating = self.officialRating {
|
||||
displayText.append("\(rating)")
|
||||
}
|
||||
|
||||
return LiveTVChannelViewProgram(timeDisplay: timeText, title: displayText)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,129 @@ import Stinsen
|
|||
import SwiftUI
|
||||
|
||||
struct LiveTVProgramsView: View {
|
||||
@EnvironmentObject
|
||||
var programsRouter: LiveTVProgramsCoordinator.Router
|
||||
@StateObject
|
||||
var viewModel = LiveTVProgramsViewModel()
|
||||
|
||||
var body: some View {
|
||||
Text("Coming Soon")
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
if !viewModel.recommendedItems.isEmpty,
|
||||
let items = viewModel.recommendedItems
|
||||
{
|
||||
PortraitImageHStackView(items: items,
|
||||
horizontalAlignment: .leading) {
|
||||
Text("On Now")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
} selectedAction: { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.seriesItems.isEmpty,
|
||||
let items = viewModel.seriesItems
|
||||
{
|
||||
PortraitImageHStackView(items: items,
|
||||
horizontalAlignment: .leading) {
|
||||
Text("Shows")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
} selectedAction: { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.movieItems.isEmpty,
|
||||
let items = viewModel.movieItems
|
||||
{
|
||||
PortraitImageHStackView(items: items,
|
||||
horizontalAlignment: .leading) {
|
||||
Text("Movies")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
} selectedAction: { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.sportsItems.isEmpty,
|
||||
let items = viewModel.sportsItems
|
||||
{
|
||||
PortraitImageHStackView(items: items,
|
||||
horizontalAlignment: .leading) {
|
||||
Text("Sports")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
} selectedAction: { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.kidsItems.isEmpty,
|
||||
let items = viewModel.kidsItems
|
||||
{
|
||||
PortraitImageHStackView(items: items,
|
||||
horizontalAlignment: .leading) {
|
||||
Text("Kids")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
} selectedAction: { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !viewModel.newsItems.isEmpty,
|
||||
let items = viewModel.newsItems
|
||||
{
|
||||
PortraitImageHStackView(items: items,
|
||||
horizontalAlignment: .leading) {
|
||||
Text("News")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.leading, 90)
|
||||
} selectedAction: { item in
|
||||
if let chanId = item.channelId,
|
||||
let chan = viewModel.findChannel(id: chanId)
|
||||
{
|
||||
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
|
||||
self.programsRouter.route(to: \.videoPlayer, playerViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,12 @@ struct ExperimentalSettingsView: View {
|
|||
var syncSubtitleStateWithAdjacent
|
||||
@Default(.Experimental.nativePlayer)
|
||||
var nativePlayer
|
||||
@Default(.Experimental.liveTVAlphaEnabled)
|
||||
var liveTVAlphaEnabled
|
||||
@Default(.Experimental.liveTVForceDirectPlay)
|
||||
var liveTVForceDirectPlay
|
||||
@Default(.Experimental.liveTVNativePlayer)
|
||||
var liveTVNativePlayer
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
@ -31,6 +37,18 @@ struct ExperimentalSettingsView: View {
|
|||
} header: {
|
||||
L10n.experimental.text
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled)
|
||||
|
||||
Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay)
|
||||
|
||||
Toggle("Live TV Native Player", isOn: $liveTVNativePlayer)
|
||||
|
||||
} header: {
|
||||
Text("Live TV")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import UIKit
|
||||
|
||||
class LiveTVNativePlayerViewController: AVPlayerViewController {
|
||||
|
||||
let viewModel: VideoPlayerViewModel
|
||||
|
||||
var timeObserverToken: Any?
|
||||
|
||||
var lastProgressTicks: Int64 = 0
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(viewModel: VideoPlayerViewModel) {
|
||||
|
||||
self.viewModel = viewModel
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
let player: AVPlayer
|
||||
|
||||
if let transcodedStreamURL = viewModel.transcodedStreamURL {
|
||||
player = AVPlayer(url: transcodedStreamURL)
|
||||
} else {
|
||||
player = AVPlayer(url: viewModel.hlsStreamURL)
|
||||
}
|
||||
|
||||
player.appliesMediaSelectionCriteriaAutomatically = false
|
||||
|
||||
let timeScale = CMTimeScale(NSEC_PER_SEC)
|
||||
let time = CMTime(seconds: 5, preferredTimescale: timeScale)
|
||||
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
|
||||
if time.seconds != 0 {
|
||||
self?.sendProgressReport(seconds: time.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
self.player = player
|
||||
|
||||
self.allowsPictureInPicturePlayback = true
|
||||
self.player?.allowsExternalPlayback = true
|
||||
}
|
||||
|
||||
private func createMetadataItem(for identifier: AVMetadataIdentifier,
|
||||
value: Any) -> AVMetadataItem
|
||||
{
|
||||
let item = AVMutableMetadataItem()
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
// Specify "und" to indicate an undefined language.
|
||||
item.extendedLanguageTag = "und"
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
stop()
|
||||
removePeriodicTimeObserver()
|
||||
}
|
||||
|
||||
func removePeriodicTimeObserver() {
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player?.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
player?.seek(to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000),
|
||||
toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, timescale: 1),
|
||||
completionHandler: { _ in
|
||||
self.play()
|
||||
})
|
||||
}
|
||||
|
||||
private func play() {
|
||||
player?.play()
|
||||
|
||||
viewModel.sendPlayReport()
|
||||
}
|
||||
|
||||
private func sendProgressReport(seconds: Double) {
|
||||
viewModel.setSeconds(Int64(seconds))
|
||||
viewModel.sendProgressReport()
|
||||
}
|
||||
|
||||
private func stop() {
|
||||
self.player?.pause()
|
||||
viewModel.sendStopReport()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// 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 (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct LiveTVNativePlayerView: UIViewControllerRepresentable {
|
||||
|
||||
let viewModel: VideoPlayerViewModel
|
||||
|
||||
typealias UIViewControllerType = LiveTVNativePlayerViewController
|
||||
|
||||
func makeUIViewController(context: Context) -> LiveTVNativePlayerViewController {
|
||||
|
||||
LiveTVNativePlayerViewController(viewModel: viewModel)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: LiveTVNativePlayerViewController, context: Context) {}
|
||||
}
|
||||
|
||||
struct LiveTVPlayerView: UIViewControllerRepresentable {
|
||||
|
||||
let viewModel: VideoPlayerViewModel
|
||||
|
||||
typealias UIViewControllerType = LiveTVPlayerViewController
|
||||
|
||||
func makeUIViewController(context: Context) -> LiveTVPlayerViewController {
|
||||
|
||||
LiveTVPlayerViewController(viewModel: viewModel)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: LiveTVPlayerViewController, context: Context) {}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue