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
|
var search = makeSearch
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var library = makeLibrary
|
var library = makeLibrary
|
||||||
|
#if os(iOS)
|
||||||
|
@Route(.push)
|
||||||
|
var liveTV = makeLiveTV
|
||||||
|
#endif
|
||||||
|
|
||||||
let viewModel: LibraryListViewModel
|
let viewModel: LibraryListViewModel
|
||||||
|
|
||||||
|
@ -35,6 +39,12 @@ final class LibraryListCoordinator: NavigationCoordinatable {
|
||||||
SearchCoordinator(viewModel: viewModel)
|
SearchCoordinator(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
func makeLiveTV() -> LiveTVCoordinator {
|
||||||
|
LiveTVCoordinator()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
func makeStart() -> some View {
|
||||||
LibraryListView(viewModel: self.viewModel)
|
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
|
@Root
|
||||||
var start = makeStart
|
var start = makeStart
|
||||||
|
@Root
|
||||||
|
var rootLibrary = makeRootLibrary
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var library = makeLibrary
|
var library = makeLibrary
|
||||||
|
|
||||||
|
@ -36,4 +38,8 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable {
|
||||||
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
||||||
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
|
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
|
@Root
|
||||||
var start = makeStart
|
var start = makeStart
|
||||||
|
@Root
|
||||||
|
var rootLibrary = makeRootLibrary
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var library = makeLibrary
|
var library = makeLibrary
|
||||||
|
|
||||||
|
@ -36,4 +38,8 @@ final class TVLibrariesCoordinator: NavigationCoordinatable {
|
||||||
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
|
||||||
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
|
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 boxset = "BoxSet"
|
||||||
case collectionFolder = "CollectionFolder"
|
case collectionFolder = "CollectionFolder"
|
||||||
case folder = "Folder"
|
case folder = "Folder"
|
||||||
|
case liveTV = "LiveTV"
|
||||||
|
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
|
@ -247,6 +248,21 @@ public extension BaseItemDto {
|
||||||
return true
|
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 {
|
var itemType: ItemType {
|
||||||
|
@ -258,7 +274,7 @@ public extension BaseItemDto {
|
||||||
|
|
||||||
func portraitHeaderViewURL(maxWidth: Int) -> URL {
|
func portraitHeaderViewURL(maxWidth: Int) -> URL {
|
||||||
switch itemType {
|
switch itemType {
|
||||||
case .movie, .season, .series, .boxset, .collectionFolder, .folder:
|
case .movie, .season, .series, .boxset, .collectionFolder, .folder, .liveTV:
|
||||||
return getPrimaryImage(maxWidth: maxWidth)
|
return getPrimaryImage(maxWidth: maxWidth)
|
||||||
case .episode:
|
case .episode:
|
||||||
return getSeriesPrimaryImage(maxWidth: maxWidth)
|
return getSeriesPrimaryImage(maxWidth: maxWidth)
|
||||||
|
|
|
@ -188,6 +188,10 @@ extension UIScreen {
|
||||||
let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width
|
let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width
|
||||||
let itemSize = width * height
|
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 {
|
struct LiveTVChannelProgram: Hashable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
let channel: BaseItemDto
|
let channel: BaseItemDto
|
||||||
let program: BaseItemDto?
|
let currentProgram: BaseItemDto?
|
||||||
|
let programs: [BaseItemDto]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class LiveTVChannelsViewModel: ViewModel {
|
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
|
return channelPrograms
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,8 @@ final class MovieLibrariesViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
self.rows = self.calculateRows()
|
self.rows = self.calculateRows()
|
||||||
if self.libraries.count == 1, let library = self.libraries.first {
|
if self.libraries.count == 1, let library = self.libraries.first {
|
||||||
// show library
|
// make this library the root of this stack
|
||||||
self.router?.route(to: \.library, library)
|
self.router?.coordinator.root(\.rootLibrary, library)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -54,8 +54,8 @@ final class TVLibrariesViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
self.rows = self.calculateRows()
|
self.rows = self.calculateRows()
|
||||||
if self.libraries.count == 1, let library = self.libraries.first {
|
if self.libraries.count == 1, let library = self.libraries.first {
|
||||||
// show library
|
// make this library the root of this stack
|
||||||
self.router?.route(to: \.library, library)
|
self.router?.coordinator.root(\.rootLibrary, library)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryListView: View {
|
struct LibraryListView: View {
|
||||||
|
@ -21,7 +23,13 @@ struct LibraryListView: View {
|
||||||
@Default(.Experimental.liveTVAlphaEnabled)
|
@Default(.Experimental.liveTVAlphaEnabled)
|
||||||
var 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 {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -30,58 +38,37 @@ struct LibraryListView: View {
|
||||||
|
|
||||||
ForEach(viewModel.libraries.filter { [self] library in
|
ForEach(viewModel.libraries.filter { [self] library in
|
||||||
let collectionType = library.collectionType ?? "other"
|
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
|
}, id: \.id) { library in
|
||||||
if library.collectionType == "livetv" {
|
Button {
|
||||||
if liveTVAlphaEnabled {
|
let itemType = BaseItemDto.ItemType(rawValue: library.collectionType ?? "other") ?? .unknown
|
||||||
Button {
|
if itemType == .liveTV {
|
||||||
self.mainCoordinator.root(\.liveTV)
|
self.mainCoordinator.root(\.liveTV)
|
||||||
}
|
} else {
|
||||||
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 {
|
|
||||||
self.libraryListRouter.route(to: \.library,
|
self.libraryListRouter.route(to: \.library,
|
||||||
(viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? ""))
|
(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 {
|
} else {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
|
|
@ -27,7 +27,7 @@ struct LibraryView: View {
|
||||||
var isShowingFilterView = false
|
var isShowingFilterView = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if viewModel.isLoading == true {
|
if viewModel.rows.isEmpty && viewModel.isLoading == true {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else if !viewModel.rows.isEmpty {
|
} else if !viewModel.rows.isEmpty {
|
||||||
CollectionView(rows: viewModel.rows) { _, _ in
|
CollectionView(rows: viewModel.rows) { _, _ in
|
||||||
|
|
|
@ -18,14 +18,25 @@ struct LiveTVChannelItemElement: View {
|
||||||
private var isFocused: Bool = false
|
private var isFocused: Bool = false
|
||||||
|
|
||||||
var channel: BaseItemDto
|
var channel: BaseItemDto
|
||||||
var program: BaseItemDto?
|
var currentProgram: BaseItemDto?
|
||||||
var startString = " "
|
var currentProgramText: LiveTVChannelViewProgram
|
||||||
var endString = " "
|
var nextProgramsText: [LiveTVChannelViewProgram]
|
||||||
var progressPercent = Double(0)
|
|
||||||
var onSelect: (@escaping (Bool) -> Void) -> Void
|
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 {
|
private var detailText: String {
|
||||||
guard let program = program else {
|
guard let program = currentProgram else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var text = ""
|
var text = ""
|
||||||
|
@ -60,61 +71,62 @@ struct LiveTVChannelItemElement: View {
|
||||||
}.frame(alignment: .top)
|
}.frame(alignment: .top)
|
||||||
Spacer()
|
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)
|
programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title,
|
||||||
.font(.footnote)
|
color: Color("TextHighlightColor"), font: Font.system(size: 20, weight: .bold, design: .default))
|
||||||
.lineLimit(1)
|
if !nextProgramsText.isEmpty,
|
||||||
.frame(alignment: .trailing)
|
let nextItem = nextProgramsText[0]
|
||||||
}
|
{
|
||||||
GeometryReader { gp in
|
programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray,
|
||||||
ZStack(alignment: .leading) {
|
font: Font.system(size: 20, design: .default))
|
||||||
RoundedRectangle(cornerRadius: 6)
|
}
|
||||||
.fill(Color.gray)
|
if nextProgramsText.count > 1,
|
||||||
.opacity(0.4)
|
let nextItem2 = nextProgramsText[1]
|
||||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12)
|
{
|
||||||
RoundedRectangle(cornerRadius: 6)
|
programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray,
|
||||||
.fill(Color.jellyfinPurple)
|
font: Font.system(size: 20, design: .default))
|
||||||
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
|
|
||||||
}
|
|
||||||
.frame(alignment: .bottom)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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 {
|
if loading {
|
||||||
ProgressView()
|
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)
|
.overlay(RoundedRectangle(cornerRadius: 20)
|
||||||
.stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4))
|
.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 SwiftUI
|
||||||
import SwiftUICollection
|
import SwiftUICollection
|
||||||
|
|
||||||
|
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
|
||||||
|
|
||||||
struct LiveTVChannelsView: View {
|
struct LiveTVChannelsView: View {
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var router: LiveTVChannelsCoordinator.Router
|
var router: LiveTVChannelsCoordinator.Router
|
||||||
|
@ -53,23 +55,30 @@ struct LiveTVChannelsView: View {
|
||||||
func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
|
func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
|
||||||
let item = cell.item
|
let item = cell.item
|
||||||
let channel = item.channel
|
let channel = item.channel
|
||||||
if channel.type != "Folder" {
|
let currentProgramDisplayText = item.currentProgram?
|
||||||
let progressPercent = item.program?.getLiveProgressPercentage() ?? 0
|
.programDisplayText(timeFormatter: viewModel.timeFormatter) ?? LiveTVChannelViewProgram(timeDisplay: "", title: "")
|
||||||
LiveTVChannelItemElement(channel: channel,
|
let nextItems = item.programs.filter { program in
|
||||||
program: item.program,
|
guard let start = program.startDate else {
|
||||||
startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ",
|
return false
|
||||||
endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ",
|
}
|
||||||
progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent,
|
guard let currentStart = item.currentProgram?.startDate else {
|
||||||
onSelect: { loadingAction in
|
return false
|
||||||
loadingAction(true)
|
}
|
||||||
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
|
return start > currentStart
|
||||||
self.router.route(to: \.videoPlayer, playerViewModel)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
||||||
loadingAction(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
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 {
|
private func createGridLayout() -> NSCollectionLayoutSection {
|
||||||
|
@ -100,4 +109,43 @@ struct LiveTVChannelsView: View {
|
||||||
|
|
||||||
return section
|
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 */; };
|
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; };
|
||||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; };
|
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; };
|
||||||
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
|
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 */; };
|
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; };
|
||||||
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; };
|
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; };
|
||||||
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.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 */; };
|
C453497F279A2DA50045F1E2 /* LiveTVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C453497E279A2DA50045F1E2 /* LiveTVPlayerViewController.swift */; };
|
||||||
C4534981279A3F140045F1E2 /* tvOSLiveTVOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */; };
|
C4534981279A3F140045F1E2 /* tvOSLiveTVOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */; };
|
||||||
C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */; };
|
C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */; };
|
||||||
C4534985279A40C60045F1E2 /* LiveTVVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534984279A40C50045F1E2 /* LiveTVVideoPlayerView.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 */; };
|
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||||
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; };
|
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; };
|
||||||
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.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 */; };
|
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
||||||
C4BE078C272844AF003F4AD1 /* 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 */; };
|
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 */; };
|
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 */; };
|
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */; };
|
||||||
E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */; };
|
E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */; };
|
||||||
E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; };
|
E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; };
|
||||||
E1002B652793CEE800E47059 /* 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 */; };
|
E1A2C15D279A7D9F005EC829 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15B279A7D9F005EC829 /* AppIcon.swift */; };
|
||||||
E1A2C15E279A7D9F005EC829 /* 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 */; };
|
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 */; };
|
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButtonView.swift */; };
|
||||||
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
||||||
E1AA33202782639D00F6439C /* 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
||||||
|
@ -739,6 +753,11 @@
|
||||||
C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVOverlay.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -894,6 +914,7 @@
|
||||||
files = (
|
files = (
|
||||||
62666E1727E501CC00EC0ECD /* CFNetwork.framework in Frameworks */,
|
62666E1727E501CC00EC0ECD /* CFNetwork.framework in Frameworks */,
|
||||||
E11D83AF278FA998006E9776 /* NukeUI in Frameworks */,
|
E11D83AF278FA998006E9776 /* NukeUI in Frameworks */,
|
||||||
|
C409CE9C284EA6EA00CABC12 /* SwiftUICollection in Frameworks */,
|
||||||
62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */,
|
62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */,
|
||||||
62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */,
|
62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */,
|
||||||
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */,
|
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */,
|
||||||
|
@ -911,7 +932,6 @@
|
||||||
62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */,
|
62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */,
|
||||||
62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */,
|
62666E2E27E5021400EC0ECD /* Security.framework in Frameworks */,
|
||||||
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
|
||||||
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */,
|
|
||||||
62666E1F27E501DF00EC0ECD /* CoreText.framework in Frameworks */,
|
62666E1F27E501DF00EC0ECD /* CoreText.framework in Frameworks */,
|
||||||
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
|
||||||
62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */,
|
62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */,
|
||||||
|
@ -928,6 +948,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
C4D0CE4B2848570700345D11 /* ASCollectionView in Frameworks */,
|
||||||
62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */,
|
62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */,
|
||||||
62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */,
|
62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */,
|
||||||
E13DD3D327168E65009D4DAF /* Defaults in Frameworks */,
|
E13DD3D327168E65009D4DAF /* Defaults in Frameworks */,
|
||||||
|
@ -944,7 +965,7 @@
|
||||||
62666E1027E501B400EC0ECD /* VideoToolbox.framework in Frameworks */,
|
62666E1027E501B400EC0ECD /* VideoToolbox.framework in Frameworks */,
|
||||||
62666E3C27E503F200EC0ECD /* GoogleCastSDK.xcframework in Frameworks */,
|
62666E3C27E503F200EC0ECD /* GoogleCastSDK.xcframework in Frameworks */,
|
||||||
62666E0C27E501A500EC0ECD /* OpenGLES.framework in Frameworks */,
|
62666E0C27E501A500EC0ECD /* OpenGLES.framework in Frameworks */,
|
||||||
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */,
|
C409CE9E285044C800CABC12 /* SwiftUICollection in Frameworks */,
|
||||||
62666E0127E5016900EC0ECD /* CoreFoundation.framework in Frameworks */,
|
62666E0127E5016900EC0ECD /* CoreFoundation.framework in Frameworks */,
|
||||||
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */,
|
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */,
|
||||||
62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */,
|
62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */,
|
||||||
|
@ -1475,6 +1496,7 @@
|
||||||
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
|
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
|
||||||
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */,
|
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */,
|
||||||
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */,
|
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */,
|
||||||
|
C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */,
|
||||||
E193D5412719404B00900D82 /* MainCoordinator */,
|
E193D5412719404B00900D82 /* MainCoordinator */,
|
||||||
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
|
||||||
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
||||||
|
@ -1632,6 +1654,9 @@
|
||||||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
|
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
|
||||||
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
6213388F265F83A900A81A2A /* LibraryListView.swift */,
|
||||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
||||||
|
C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */,
|
||||||
|
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */,
|
||||||
|
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */,
|
||||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
||||||
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
||||||
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
||||||
|
@ -1730,10 +1755,13 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */,
|
E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */,
|
||||||
|
C45640CF281A43EF007096DE /* LiveTVNativePlayerViewController.swift */,
|
||||||
E1002B692793E12E00E47059 /* Overlays */,
|
E1002B692793E12E00E47059 /* Overlays */,
|
||||||
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
|
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
|
||||||
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */,
|
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */,
|
||||||
|
C45942CC27F6994A00C54FE7 /* LiveTVPlayerView.swift */,
|
||||||
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */,
|
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */,
|
||||||
|
C45942CA27F6984100C54FE7 /* LiveTVPlayerViewController.swift */,
|
||||||
);
|
);
|
||||||
path = VideoPlayer;
|
path = VideoPlayer;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1810,6 +1838,7 @@
|
||||||
E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = {
|
E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C45942C827F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift */,
|
||||||
6220D0C526D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift */,
|
6220D0C526D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift */,
|
||||||
C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */,
|
C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */,
|
||||||
E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */,
|
E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */,
|
||||||
|
@ -1899,12 +1928,12 @@
|
||||||
E13DD3CC27164CA7009D4DAF /* CoreStore */,
|
E13DD3CC27164CA7009D4DAF /* CoreStore */,
|
||||||
E12186DD2718F1C50010884C /* Defaults */,
|
E12186DD2718F1C50010884C /* Defaults */,
|
||||||
E1218C9D271A2CD600EA0737 /* CombineExt */,
|
E1218C9D271A2CD600EA0737 /* CombineExt */,
|
||||||
E1A9999A271A343C008E78C0 /* SwiftUICollection */,
|
|
||||||
E178857C278037FD0094FBCF /* JellyfinAPI */,
|
E178857C278037FD0094FBCF /* JellyfinAPI */,
|
||||||
E1AE8E7D2789136D00FBDDAA /* Nuke */,
|
E1AE8E7D2789136D00FBDDAA /* Nuke */,
|
||||||
E11D83AE278FA998006E9776 /* NukeUI */,
|
E11D83AE278FA998006E9776 /* NukeUI */,
|
||||||
E1002B6A2793E36600E47059 /* Algorithms */,
|
E1002B6A2793E36600E47059 /* Algorithms */,
|
||||||
E1347DB5279E3CA500BC6161 /* Puppy */,
|
E1347DB5279E3CA500BC6161 /* Puppy */,
|
||||||
|
C409CE9B284EA6EA00CABC12 /* SwiftUICollection */,
|
||||||
);
|
);
|
||||||
productName = "JellyfinPlayer tvOS";
|
productName = "JellyfinPlayer tvOS";
|
||||||
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
|
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
|
||||||
|
@ -1936,7 +1965,6 @@
|
||||||
E13DD3D227168E65009D4DAF /* Defaults */,
|
E13DD3D227168E65009D4DAF /* Defaults */,
|
||||||
E1B6DCE7271A23780015B715 /* CombineExt */,
|
E1B6DCE7271A23780015B715 /* CombineExt */,
|
||||||
E1B6DCE9271A23880015B715 /* SwiftyJSON */,
|
E1B6DCE9271A23880015B715 /* SwiftyJSON */,
|
||||||
E1A99998271A3429008E78C0 /* SwiftUICollection */,
|
|
||||||
E10EAA44277BB646000269ED /* JellyfinAPI */,
|
E10EAA44277BB646000269ED /* JellyfinAPI */,
|
||||||
E10EAA4C277BB716000269ED /* Sliders */,
|
E10EAA4C277BB716000269ED /* Sliders */,
|
||||||
E1AE8E7B2789135A00FBDDAA /* Nuke */,
|
E1AE8E7B2789135A00FBDDAA /* Nuke */,
|
||||||
|
@ -1944,6 +1972,8 @@
|
||||||
E1002B672793CFBA00E47059 /* Algorithms */,
|
E1002B672793CFBA00E47059 /* Algorithms */,
|
||||||
62666E3827E502CE00EC0ECD /* SwizzleSwift */,
|
62666E3827E502CE00EC0ECD /* SwizzleSwift */,
|
||||||
E1101176281B1E8A006A3584 /* Puppy */,
|
E1101176281B1E8A006A3584 /* Puppy */,
|
||||||
|
C4D0CE4A2848570700345D11 /* ASCollectionView */,
|
||||||
|
C409CE9D285044C800CABC12 /* SwiftUICollection */,
|
||||||
);
|
);
|
||||||
productName = JellyfinPlayer;
|
productName = JellyfinPlayer;
|
||||||
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
|
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
|
||||||
|
@ -2030,7 +2060,6 @@
|
||||||
E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */,
|
E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */,
|
||||||
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */,
|
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */,
|
||||||
E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||||
C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
|
|
||||||
E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
|
E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
|
||||||
E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */,
|
E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */,
|
||||||
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */,
|
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||||
|
@ -2038,6 +2067,8 @@
|
||||||
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
|
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
|
||||||
62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */,
|
62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */,
|
||||||
E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */,
|
E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */,
|
||||||
|
C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */,
|
||||||
|
C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -2069,6 +2100,7 @@
|
||||||
53913C0526D323FE00EB3286 /* Localizable.strings in Resources */,
|
53913C0526D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||||
53913BFF26D323FE00EB3286 /* Localizable.strings in Resources */,
|
53913BFF26D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||||
53913C0E26D323FE00EB3286 /* Localizable.strings in Resources */,
|
53913C0E26D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||||
|
C4464953281616AE00DDB461 /* Assets.xcassets in Resources */,
|
||||||
53913BF026D323FE00EB3286 /* Localizable.strings in Resources */,
|
53913BF026D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||||
53913C0826D323FE00EB3286 /* Localizable.strings in Resources */,
|
53913C0826D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||||
53913C1126D323FE00EB3286 /* Localizable.strings in Resources */,
|
53913C1126D323FE00EB3286 /* Localizable.strings in Resources */,
|
||||||
|
@ -2243,6 +2275,7 @@
|
||||||
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||||
E193D54B271941D300900D82 /* ServerListView.swift in Sources */,
|
E193D54B271941D300900D82 /* ServerListView.swift in Sources */,
|
||||||
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
|
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
|
||||||
|
C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */,
|
||||||
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */,
|
6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */,
|
||||||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||||
|
@ -2268,7 +2301,6 @@
|
||||||
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
|
||||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||||
E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */,
|
E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */,
|
||||||
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */,
|
|
||||||
E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
|
E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
|
||||||
E1A2C15E279A7D9F005EC829 /* AppIcon.swift in Sources */,
|
E1A2C15E279A7D9F005EC829 /* AppIcon.swift in Sources */,
|
||||||
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
|
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
|
||||||
|
@ -2378,9 +2410,11 @@
|
||||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||||
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
|
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
|
||||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||||
|
C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */,
|
||||||
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
|
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
|
||||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||||
53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */,
|
53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */,
|
||||||
|
C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */,
|
||||||
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||||
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
||||||
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
||||||
|
@ -2399,9 +2433,11 @@
|
||||||
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
|
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */,
|
E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */,
|
||||||
|
C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */,
|
||||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||||
E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */,
|
E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */,
|
||||||
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
||||||
|
C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */,
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
||||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||||
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
|
||||||
|
@ -2413,12 +2449,14 @@
|
||||||
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */,
|
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */,
|
||||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||||
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
|
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
|
||||||
|
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */,
|
||||||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
||||||
E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */,
|
E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */,
|
||||||
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */,
|
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */,
|
||||||
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */,
|
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */,
|
||||||
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */,
|
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */,
|
||||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
||||||
|
C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */,
|
||||||
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
||||||
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
||||||
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
|
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
|
||||||
|
@ -2469,12 +2507,14 @@
|
||||||
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
||||||
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
||||||
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */,
|
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */,
|
||||||
|
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */,
|
||||||
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */,
|
||||||
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
||||||
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */,
|
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */,
|
||||||
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
|
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
|
||||||
E1A2C154279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
|
E1A2C154279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
|
||||||
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
|
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
|
||||||
|
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */,
|
||||||
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
|
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
|
||||||
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
|
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||||
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
|
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
|
||||||
|
@ -2486,7 +2526,9 @@
|
||||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||||
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||||
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
|
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||||
|
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */,
|
||||||
E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
||||||
|
C45640D0281A43EF007096DE /* LiveTVNativePlayerViewController.swift in Sources */,
|
||||||
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||||
|
@ -3083,14 +3125,22 @@
|
||||||
minimumVersion = 2.0.2;
|
minimumVersion = 2.0.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */ = {
|
C409CE9A284EA6EA00CABC12 /* XCRemoteSwiftPackageReference "SwiftUICollection" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/ABJC/SwiftUICollection";
|
repositoryURL = "https://github.com/defagos/SwiftUICollection";
|
||||||
requirement = {
|
requirement = {
|
||||||
branch = master;
|
branch = master;
|
||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
C4D0CE492848570700345D11 /* XCRemoteSwiftPackageReference "ASCollectionView" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/apptekstudios/ASCollectionView";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
|
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/apple/swift-algorithms.git";
|
repositoryURL = "https://github.com/apple/swift-algorithms.git";
|
||||||
|
@ -3227,6 +3277,21 @@
|
||||||
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
||||||
productName = 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 */ = {
|
E1002B672793CFBA00E47059 /* Algorithms */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
|
package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
|
||||||
|
@ -3322,16 +3387,6 @@
|
||||||
package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */;
|
package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */;
|
||||||
productName = JellyfinAPI;
|
productName = JellyfinAPI;
|
||||||
};
|
};
|
||||||
E1A99998271A3429008E78C0 /* SwiftUICollection */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
|
||||||
productName = SwiftUICollection;
|
|
||||||
};
|
|
||||||
E1A9999A271A343C008E78C0 /* SwiftUICollection */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
|
|
||||||
productName = SwiftUICollection;
|
|
||||||
};
|
|
||||||
E1AE8E7B2789135A00FBDDAA /* Nuke */ = {
|
E1AE8E7B2789135A00FBDDAA /* Nuke */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
|
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||||
|
|
|
@ -18,6 +18,15 @@
|
||||||
"version" : "0.6.4"
|
"version" : "0.6.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "ascollectionview",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apptekstudios/ASCollectionView",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4288744ba484c1062c109c0f28d72b629d321d55",
|
||||||
|
"version" : "2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "combineext",
|
"identity" : "combineext",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@ -45,6 +54,15 @@
|
||||||
"version" : "6.2.1"
|
"version" : "6.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "differencekit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/ra1028/DifferenceKit",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "62745d7780deef4a023a792a1f8f763ec7bf9705",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "gifu",
|
"identity" : "gifu",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@ -156,10 +174,10 @@
|
||||||
{
|
{
|
||||||
"identity" : "swiftuicollection",
|
"identity" : "swiftuicollection",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/ABJC/SwiftUICollection",
|
"location" : "https://github.com/defagos/SwiftUICollection",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "master",
|
"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
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
import Stinsen
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@ -16,7 +18,16 @@ struct LibraryListView: View {
|
||||||
@StateObject
|
@StateObject
|
||||||
var viewModel = LibraryListViewModel()
|
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 {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -46,12 +57,18 @@ struct LibraryListView: View {
|
||||||
if !viewModel.isLoading {
|
if !viewModel.isLoading {
|
||||||
ForEach(viewModel.libraries.filter { [self] library in
|
ForEach(viewModel.libraries.filter { [self] library in
|
||||||
let collectionType = library.collectionType ?? "other"
|
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
|
}, id: \.id) { library in
|
||||||
Button {
|
Button {
|
||||||
libraryListRouter.route(to: \.library,
|
let itemType = BaseItemDto.ItemType(rawValue: library.collectionType ?? "other") ?? .unknown
|
||||||
(viewModel: LibraryViewModel(parentID: library.id),
|
if itemType == .liveTV {
|
||||||
title: library.name ?? ""))
|
libraryListRouter.route(to: \.liveTV)
|
||||||
|
} else {
|
||||||
|
libraryListRouter.route(to: \.library,
|
||||||
|
(viewModel: LibraryViewModel(parentID: library.id),
|
||||||
|
title: library.name ?? ""))
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
ImageView(library.getPrimaryImage(maxWidth: 500), blurHash: library.getPrimaryImageBlurHash())
|
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
|
import SwiftUI
|
||||||
|
|
||||||
struct LiveTVProgramsView: View {
|
struct LiveTVProgramsView: View {
|
||||||
|
@EnvironmentObject
|
||||||
|
var programsRouter: LiveTVProgramsCoordinator.Router
|
||||||
|
@StateObject
|
||||||
|
var viewModel = LiveTVProgramsViewModel()
|
||||||
|
|
||||||
var body: some View {
|
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
|
var syncSubtitleStateWithAdjacent
|
||||||
@Default(.Experimental.nativePlayer)
|
@Default(.Experimental.nativePlayer)
|
||||||
var nativePlayer
|
var nativePlayer
|
||||||
|
@Default(.Experimental.liveTVAlphaEnabled)
|
||||||
|
var liveTVAlphaEnabled
|
||||||
|
@Default(.Experimental.liveTVForceDirectPlay)
|
||||||
|
var liveTVForceDirectPlay
|
||||||
|
@Default(.Experimental.liveTVNativePlayer)
|
||||||
|
var liveTVNativePlayer
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
@ -31,6 +37,18 @@ struct ExperimentalSettingsView: View {
|
||||||
} header: {
|
} header: {
|
||||||
L10n.experimental.text
|
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