Live TV Updates (#1022)
This commit is contained in:
parent
913dda5fea
commit
ec9bfaa2fe
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A view that takes a view to affect layout while overlaying the content.
|
||||||
|
struct AlternateLayoutView<Content: View, Layout: View>: View {
|
||||||
|
|
||||||
|
private let alignment: Alignment
|
||||||
|
private let content: () -> Content
|
||||||
|
private let layout: () -> Layout
|
||||||
|
|
||||||
|
init(
|
||||||
|
alignment: Alignment = .center,
|
||||||
|
@ViewBuilder layout: @escaping () -> Layout,
|
||||||
|
@ViewBuilder content: @escaping () -> Content
|
||||||
|
) {
|
||||||
|
self.alignment = alignment
|
||||||
|
self.content = content
|
||||||
|
self.layout = layout
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
layout()
|
||||||
|
.hidden()
|
||||||
|
.overlay(alignment: alignment) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct PlainNavigationLinkButtonStyle: ButtonStyle {
|
|
||||||
func makeBody(configuration: Self.Configuration) -> some View {
|
|
||||||
PlainNavigationLinkButton(configuration: configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PlainNavigationLinkButton: View {
|
|
||||||
let configuration: ButtonStyle.Configuration
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
configuration.label
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,8 +8,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// TODO: Replace scaling with size so that the Capsule corner radius
|
// TODO: see if animation is correct here or should be in caller views
|
||||||
// is not affected
|
|
||||||
|
|
||||||
struct ProgressBar: View {
|
struct ProgressBar: View {
|
||||||
|
|
||||||
|
@ -18,12 +17,15 @@ struct ProgressBar: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
Capsule()
|
Capsule()
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.secondary)
|
||||||
.opacity(0.2)
|
.opacity(0.2)
|
||||||
|
|
||||||
Capsule()
|
Capsule()
|
||||||
.scaleEffect(x: progress, y: 1, anchor: .leading)
|
.mask(alignment: .leading) {
|
||||||
|
Rectangle()
|
||||||
|
.scaleEffect(x: progress, anchor: .leading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.animation(.linear(duration: 0.1), value: progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,20 +8,28 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: is the background color setting really the best way?
|
||||||
|
|
||||||
struct SystemImageContentView: View {
|
struct SystemImageContentView: View {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var contentSize: CGSize = .zero
|
private var contentSize: CGSize = .zero
|
||||||
|
|
||||||
|
private var backgroundColor: Color
|
||||||
|
private var heightRatio: CGFloat
|
||||||
private let systemName: String
|
private let systemName: String
|
||||||
|
private var widthRatio: CGFloat
|
||||||
|
|
||||||
init(systemName: String?) {
|
init(systemName: String?) {
|
||||||
|
self.backgroundColor = Color.secondarySystemFill
|
||||||
|
self.heightRatio = 3
|
||||||
self.systemName = systemName ?? "circle"
|
self.systemName = systemName ?? "circle"
|
||||||
|
self.widthRatio = 3.5
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.secondarySystemFill
|
backgroundColor
|
||||||
.opacity(0.5)
|
.opacity(0.5)
|
||||||
|
|
||||||
Image(systemName: systemName)
|
Image(systemName: systemName)
|
||||||
|
@ -29,8 +37,20 @@ struct SystemImageContentView: View {
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
.frame(width: contentSize.width / 3.5, height: contentSize.height / 3)
|
.frame(width: contentSize.width / widthRatio, height: contentSize.height / heightRatio)
|
||||||
}
|
}
|
||||||
.size($contentSize)
|
.size($contentSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SystemImageContentView {
|
||||||
|
|
||||||
|
func background(color: Color = Color.secondarySystemFill) -> Self {
|
||||||
|
copy(modifying: \.backgroundColor, with: color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageFrameRatio(width: CGFloat = 3.5, height: CGFloat = 3) -> Self {
|
||||||
|
copy(modifying: \.heightRatio, with: height)
|
||||||
|
.copy(modifying: \.widthRatio, with: width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,8 +18,15 @@ final class LiveTVCoordinator: NavigationCoordinatable {
|
||||||
@Root
|
@Root
|
||||||
var start = makeStart
|
var start = makeStart
|
||||||
|
|
||||||
|
@Route(.push)
|
||||||
|
var channels = makeChannels
|
||||||
|
|
||||||
|
func makeChannels() -> ChannelLibraryView {
|
||||||
|
ChannelLibraryView()
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
func makeStart() -> some View {
|
||||||
LiveTVChannelsView()
|
ProgramsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class LiveTVCoordinator: TabCoordinatable {
|
||||||
|
|
||||||
|
var child = TabChild(startingItems: [
|
||||||
|
\LiveTVCoordinator.programs,
|
||||||
|
\LiveTVCoordinator.channels,
|
||||||
|
])
|
||||||
|
|
||||||
|
@Route(tabItem: makeProgramsTab)
|
||||||
|
var programs = makePrograms
|
||||||
|
@Route(tabItem: makeChannelsTab)
|
||||||
|
var channels = makeChannels
|
||||||
|
|
||||||
|
func makePrograms() -> VideoPlayerWrapperCoordinator {
|
||||||
|
VideoPlayerWrapperCoordinator {
|
||||||
|
ProgramsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeProgramsTab(isActive: Bool) -> some View {
|
||||||
|
Label(L10n.programs, systemImage: "tv")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeChannels() -> VideoPlayerWrapperCoordinator {
|
||||||
|
VideoPlayerWrapperCoordinator {
|
||||||
|
ChannelLibraryView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeChannelsTab(isActive: Bool) -> some View {
|
||||||
|
Label(L10n.channels, systemImage: "play.square.stack")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,53 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Algorithms
|
|
||||||
import Defaults
|
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
|
||||||
import Stinsen
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
final class LiveTVProgramsCoordinator: NavigationCoordinatable {
|
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start)
|
|
||||||
|
|
||||||
@Root
|
|
||||||
var start = makeStart
|
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
@Route(.fullScreen)
|
|
||||||
var videoPlayer = makeVideoPlayer
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
func makeVideoPlayer(manager: LiveVideoPlayerManager) -> NavigationViewCoordinator<LiveVideoPlayerCoordinator> {
|
|
||||||
NavigationViewCoordinator(LiveVideoPlayerCoordinator(manager: manager))
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// @ViewBuilder
|
|
||||||
func makeStart() -> some View {
|
|
||||||
let viewModel = LiveTVProgramsViewModel()
|
|
||||||
|
|
||||||
// let channels = (1 ..< 20).map { _ in BaseItemDto.randomItem() }
|
|
||||||
//
|
|
||||||
// for channel in channels {
|
|
||||||
// viewModel.channels[channel.id!] = channel
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// viewModel.recommendedItems = channels.randomSample(count: 5)
|
|
||||||
// viewModel.seriesItems = channels.randomSample(count: 5)
|
|
||||||
// viewModel.movieItems = channels.randomSample(count: 5)
|
|
||||||
// viewModel.sportsItems = channels.randomSample(count: 5)
|
|
||||||
// viewModel.kidsItems = channels.randomSample(count: 5)
|
|
||||||
// viewModel.newsItems = channels.randomSample(count: 5)
|
|
||||||
|
|
||||||
return LiveTVProgramsView(viewModel: viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Stinsen
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
final class LiveTVTabCoordinator: TabCoordinatable {
|
|
||||||
|
|
||||||
var child = TabChild(startingItems: [
|
|
||||||
\LiveTVTabCoordinator.channels,
|
|
||||||
\LiveTVTabCoordinator.programs,
|
|
||||||
\LiveTVTabCoordinator.home,
|
|
||||||
])
|
|
||||||
|
|
||||||
@Route(tabItem: makeChannelsTab)
|
|
||||||
var channels = makeChannels
|
|
||||||
@Route(tabItem: makeProgramsTab)
|
|
||||||
var programs = makePrograms
|
|
||||||
@Route(tabItem: makeHomeTab)
|
|
||||||
var home = makeHome
|
|
||||||
|
|
||||||
func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> {
|
|
||||||
NavigationViewCoordinator(LiveTVChannelsCoordinator())
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func makeChannelsTab(isActive: Bool) -> some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "square.grid.3x3")
|
|
||||||
L10n.channels.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> {
|
|
||||||
NavigationViewCoordinator(LiveTVProgramsCoordinator())
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func makeProgramsTab(isActive: Bool) -> some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "tv")
|
|
||||||
L10n.programs.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeHome() -> LiveTVHomeView {
|
|
||||||
LiveTVHomeView()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func makeHomeTab(isActive: Bool) -> some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "house")
|
|
||||||
L10n.home.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -38,18 +38,20 @@ final class LiveVideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
LiveNativeVideoPlayer(manager: self.videoPlayerManager)
|
LiveNativeVideoPlayer(manager: self.videoPlayerManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.supportedOrientations(UIDevice.isPhone ? .landscape : .allButUpsideDown)
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
.backport
|
||||||
|
.persistentSystemOverlays(.hidden)
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
PreferencesView {
|
PreferencesView {
|
||||||
Group {
|
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
|
||||||
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
|
LiveVideoPlayer(manager: self.videoPlayerManager)
|
||||||
LiveVideoPlayer(manager: self.videoPlayerManager)
|
} else {
|
||||||
} else {
|
LiveNativeVideoPlayer(manager: self.videoPlayerManager)
|
||||||
LiveNativeVideoPlayer(manager: self.videoPlayerManager)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
|
@ -23,10 +23,6 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
var mainTab = makeMainTab
|
var mainTab = makeMainTab
|
||||||
@Root
|
@Root
|
||||||
var serverList = makeServerList
|
var serverList = makeServerList
|
||||||
@Root
|
|
||||||
var liveTV = makeLiveTV
|
|
||||||
// @Route(.fullScreen)
|
|
||||||
// var videoPlayer = makeVideoPlayer
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
||||||
|
@ -65,12 +61,4 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
|
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
|
||||||
NavigationViewCoordinator(ServerListCoordinator())
|
NavigationViewCoordinator(ServerListCoordinator())
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLiveTV() -> LiveTVTabCoordinator {
|
|
||||||
LiveTVTabCoordinator()
|
|
||||||
}
|
|
||||||
|
|
||||||
// func makeVideoPlayer(parameters: VideoPlayerCoordinator.Parameters) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
|
|
||||||
// NavigationViewCoordinator(VideoPlayerCoordinator(parameters: parameters))
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class MainTabCoordinator: TabCoordinatable {
|
final class MainTabCoordinator: TabCoordinatable {
|
||||||
|
|
||||||
var child = TabChild(startingItems: [
|
var child = TabChild(startingItems: [
|
||||||
\MainTabCoordinator.home,
|
\MainTabCoordinator.home,
|
||||||
\MainTabCoordinator.tvShows,
|
\MainTabCoordinator.tvShows,
|
||||||
|
|
|
@ -20,6 +20,8 @@ final class MediaCoordinator: NavigationCoordinatable {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@Route(.modal)
|
@Route(.modal)
|
||||||
var library = makeLibrary
|
var library = makeLibrary
|
||||||
|
@Route(.modal)
|
||||||
|
var liveTV = makeLiveTV
|
||||||
#else
|
#else
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var library = makeLibrary
|
var library = makeLibrary
|
||||||
|
@ -33,21 +35,20 @@ final class MediaCoordinator: NavigationCoordinatable {
|
||||||
func makeLibrary(viewModel: PagingLibraryViewModel<BaseItemDto>) -> NavigationViewCoordinator<LibraryCoordinator<BaseItemDto>> {
|
func makeLibrary(viewModel: PagingLibraryViewModel<BaseItemDto>) -> NavigationViewCoordinator<LibraryCoordinator<BaseItemDto>> {
|
||||||
NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel))
|
NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel))
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
func makeLibrary(viewModel: PagingLibraryViewModel<BaseItemDto>) -> LibraryCoordinator<BaseItemDto> {
|
func makeLibrary(viewModel: PagingLibraryViewModel<BaseItemDto>) -> LibraryCoordinator<BaseItemDto> {
|
||||||
LibraryCoordinator(viewModel: viewModel)
|
LibraryCoordinator(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLiveTV() -> LiveTVCoordinator {
|
|
||||||
LiveTVCoordinator()
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeDownloads() -> DownloadListCoordinator {
|
func makeDownloads() -> DownloadListCoordinator {
|
||||||
DownloadListCoordinator()
|
DownloadListCoordinator()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
func makeLiveTV() -> LiveTVCoordinator {
|
||||||
|
LiveTVCoordinator()
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
func makeStart() -> some View {
|
||||||
MediaView()
|
MediaView()
|
||||||
|
|
|
@ -49,8 +49,6 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.backport
|
.backport
|
||||||
.persistentSystemOverlays(.hidden)
|
.persistentSystemOverlays(.hidden)
|
||||||
.backport
|
|
||||||
.defersSystemGestures(on: .all)
|
|
||||||
|
|
||||||
#else
|
#else
|
||||||
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
|
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
|
||||||
|
|
|
@ -6,32 +6,36 @@
|
||||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
import Defaults
|
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
|
||||||
import Stinsen
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class LiveTVChannelsCoordinator: NavigationCoordinatable {
|
// TODO: add normal video player
|
||||||
|
// TODO: replace current instances of video player on other coordinators, if able
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start)
|
/// A coordinator used on tvOS to present video players due to differences in view controller presentation.
|
||||||
|
final class VideoPlayerWrapperCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
|
let stack = NavigationStack(initial: \VideoPlayerWrapperCoordinator.start)
|
||||||
|
|
||||||
@Root
|
@Root
|
||||||
var start = makeStart
|
var start = makeStart
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
@Route(.fullScreen)
|
@Route(.fullScreen)
|
||||||
var liveVideoPlayer = makeLiveVideoPlayer
|
var liveVideoPlayer = makeLiveVideoPlayer
|
||||||
#endif
|
|
||||||
|
|
||||||
#if os(tvOS)
|
private let content: () -> any View
|
||||||
|
|
||||||
|
init(@ViewBuilder _ content: @escaping () -> any View) {
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
func makeLiveVideoPlayer(manager: LiveVideoPlayerManager) -> NavigationViewCoordinator<LiveVideoPlayerCoordinator> {
|
func makeLiveVideoPlayer(manager: LiveVideoPlayerManager) -> NavigationViewCoordinator<LiveVideoPlayerCoordinator> {
|
||||||
NavigationViewCoordinator(LiveVideoPlayerCoordinator(manager: manager))
|
NavigationViewCoordinator(LiveVideoPlayerCoordinator(manager: manager))
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func makeStart() -> some View {
|
private func makeStart() -> some View {
|
||||||
LiveTVChannelsView()
|
content()
|
||||||
|
.eraseToAnyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -46,14 +46,16 @@ extension BaseItemDto: Poster {
|
||||||
|
|
||||||
var typeSystemImage: String? {
|
var typeSystemImage: String? {
|
||||||
switch type {
|
switch type {
|
||||||
|
case .boxSet:
|
||||||
|
"film.stack"
|
||||||
case .episode, .movie, .series:
|
case .episode, .movie, .series:
|
||||||
"film"
|
"film"
|
||||||
case .folder:
|
case .folder:
|
||||||
"folder.fill"
|
"folder.fill"
|
||||||
case .person:
|
case .person:
|
||||||
"person.fill"
|
"person.fill"
|
||||||
case .boxSet:
|
case .program:
|
||||||
"film.stack"
|
"tv"
|
||||||
default: nil
|
default: nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,6 +85,8 @@ extension BaseItemDto: Poster {
|
||||||
}
|
}
|
||||||
case .folder:
|
case .folder:
|
||||||
return [imageSource(.primary, maxWidth: maxWidth)]
|
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||||
|
case .program:
|
||||||
|
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||||
case .video:
|
case .video:
|
||||||
return [imageSource(.primary, maxWidth: maxWidth)]
|
return [imageSource(.primary, maxWidth: maxWidth)]
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
// TODO: clean up
|
||||||
|
|
||||||
extension BaseItemDto: Displayable {
|
extension BaseItemDto: Displayable {
|
||||||
|
|
||||||
var displayTitle: String {
|
var displayTitle: String {
|
||||||
|
@ -97,18 +99,27 @@ extension BaseItemDto {
|
||||||
return " "
|
return " "
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLiveProgressPercentage() -> Double {
|
var programDuration: TimeInterval? {
|
||||||
if let startDate,
|
guard let startDate, let endDate else { return nil }
|
||||||
let endDate
|
return endDate.timeIntervalSince(startDate)
|
||||||
{
|
}
|
||||||
let start = startDate.timeIntervalSinceReferenceDate
|
|
||||||
let end = endDate.timeIntervalSinceReferenceDate
|
var programProgress: Double? {
|
||||||
let now = Date().timeIntervalSinceReferenceDate
|
guard let startDate, let endDate else { return nil }
|
||||||
let length = end - start
|
|
||||||
let progress = now - start
|
let length = endDate.timeIntervalSince(startDate)
|
||||||
return progress / length
|
let progress = Date.now.timeIntervalSince(startDate)
|
||||||
}
|
|
||||||
return 0
|
return progress / length
|
||||||
|
}
|
||||||
|
|
||||||
|
func programProgress(relativeTo other: Date) -> Double? {
|
||||||
|
guard let startDate, let endDate else { return nil }
|
||||||
|
|
||||||
|
let length = endDate.timeIntervalSince(startDate)
|
||||||
|
let progress = other.timeIntervalSince(startDate)
|
||||||
|
|
||||||
|
return progress / length
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitleStreams: [MediaStream] {
|
var subtitleStreams: [MediaStream] {
|
||||||
|
|
|
@ -24,6 +24,27 @@ extension Sequence {
|
||||||
sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
|
sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the elements of the sequence, sorted by comparing values
|
||||||
|
/// at the given `KeyPath` of `Element`.
|
||||||
|
///
|
||||||
|
/// `nil` values are considered the maximum.
|
||||||
|
func sorted<Key: Comparable>(using keyPath: KeyPath<Element, Key?>) -> [Element] {
|
||||||
|
sorted {
|
||||||
|
let x = $0[keyPath: keyPath]
|
||||||
|
let y = $1[keyPath: keyPath]
|
||||||
|
|
||||||
|
if let x, let y {
|
||||||
|
return x < y
|
||||||
|
} else if let _ = x {
|
||||||
|
return true
|
||||||
|
} else if let _ = y {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func subtracting<Value: Equatable>(_ other: some Sequence<Value>, using keyPath: KeyPath<Element, Value>) -> [Element] {
|
func subtracting<Value: Equatable>(_ other: some Sequence<Value>, using keyPath: KeyPath<Element, Value>) -> [Element] {
|
||||||
filter { !other.contains($0[keyPath: keyPath]) }
|
filter { !other.contains($0[keyPath: keyPath]) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,9 @@ extension String {
|
||||||
.reduce("", +)
|
.reduce("", +)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var emptyDash = "--"
|
static let emptyDash = "--"
|
||||||
|
|
||||||
|
static let emptyTime = "--:--"
|
||||||
|
|
||||||
var shortFileName: String {
|
var shortFileName: String {
|
||||||
(split(separator: "/").last?.description ?? self)
|
(split(separator: "/").last?.description ?? self)
|
||||||
|
|
|
@ -315,4 +315,14 @@ extension View {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
// Useful modifier during development
|
||||||
|
func debugBackground(_ color: Color = Color.red, opacity: CGFloat = 0.5) -> some View {
|
||||||
|
background {
|
||||||
|
color
|
||||||
|
.opacity(opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
// Note: assumes programs are sorted by start date
|
||||||
|
|
||||||
|
/// Structure that has a channel and associated programs.
|
||||||
|
struct ChannelProgram: Hashable, Identifiable {
|
||||||
|
|
||||||
|
let channel: BaseItemDto
|
||||||
|
let programs: [BaseItemDto]
|
||||||
|
|
||||||
|
var currentProgram: BaseItemDto? {
|
||||||
|
programs.first { program in
|
||||||
|
guard let start = program.startDate,
|
||||||
|
let end = program.endDate else { return false }
|
||||||
|
|
||||||
|
return (start ... end).contains(Date.now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func programAfterCurrent(offset: Int) -> BaseItemDto? {
|
||||||
|
guard let currentStart = currentProgram?.startDate else { return nil }
|
||||||
|
|
||||||
|
return programs.filter { program in
|
||||||
|
guard let start = program.startDate else { return false }
|
||||||
|
return start > currentStart
|
||||||
|
}[safe: offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String? {
|
||||||
|
channel.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChannelProgram: Poster {
|
||||||
|
|
||||||
|
var displayTitle: String {
|
||||||
|
channel.displayTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
var subtitle: String? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeSystemImage: String? {
|
||||||
|
"tv"
|
||||||
|
}
|
||||||
|
|
||||||
|
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
|
||||||
|
channel.imageSource(.primary, maxWidth: maxWidth)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,120 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Defaults
|
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
struct LiveTVChannelProgram: Hashable {
|
|
||||||
let id = UUID()
|
|
||||||
let channel: BaseItemDto
|
|
||||||
let currentProgram: BaseItemDto?
|
|
||||||
let programs: [BaseItemDto]
|
|
||||||
}
|
|
||||||
|
|
||||||
extension LiveTVChannelProgram: Poster {
|
|
||||||
var displayTitle: String {
|
|
||||||
guard let currentProgram else { return "None" }
|
|
||||||
return currentProgram.displayTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
guard let currentProgram else { return "None" }
|
|
||||||
switch currentProgram.type {
|
|
||||||
case .episode:
|
|
||||||
return currentProgram.seriesName ?? currentProgram.displayTitle
|
|
||||||
default:
|
|
||||||
return currentProgram.displayTitle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var subtitle: String? {
|
|
||||||
guard let currentProgram else { return "" }
|
|
||||||
switch currentProgram.type {
|
|
||||||
case .episode:
|
|
||||||
return currentProgram.seasonEpisodeLabel
|
|
||||||
case .video:
|
|
||||||
return currentProgram.extraType?.displayTitle
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var showTitle: Bool {
|
|
||||||
guard let currentProgram else { return false }
|
|
||||||
switch currentProgram.type {
|
|
||||||
case .episode, .series, .movie, .boxSet, .collectionFolder:
|
|
||||||
return Defaults[.Customization.showPosterLabels]
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var typeSystemImage: String? {
|
|
||||||
guard let currentProgram else { return nil }
|
|
||||||
switch currentProgram.type {
|
|
||||||
case .episode, .movie, .series:
|
|
||||||
return "film"
|
|
||||||
case .folder:
|
|
||||||
return "folder.fill"
|
|
||||||
case .person:
|
|
||||||
return "person.fill"
|
|
||||||
case .boxSet:
|
|
||||||
return "film.stack"
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
|
|
||||||
guard let currentProgram else { return ImageSource() }
|
|
||||||
switch currentProgram.type {
|
|
||||||
case .episode:
|
|
||||||
return currentProgram.seriesImageSource(.primary, maxWidth: maxWidth)
|
|
||||||
case .folder:
|
|
||||||
return ImageSource()
|
|
||||||
default:
|
|
||||||
return currentProgram.imageSource(.primary, maxWidth: maxWidth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool = false) -> [ImageSource] {
|
|
||||||
guard let currentProgram else { return [] }
|
|
||||||
switch currentProgram.type {
|
|
||||||
case .episode:
|
|
||||||
if single || !Defaults[.Customization.Episodes.useSeriesLandscapeBackdrop] {
|
|
||||||
return [currentProgram.imageSource(.primary, maxWidth: maxWidth)]
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
currentProgram.seriesImageSource(.thumb, maxWidth: maxWidth),
|
|
||||||
currentProgram.seriesImageSource(.backdrop, maxWidth: maxWidth),
|
|
||||||
currentProgram.imageSource(.primary, maxWidth: maxWidth),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
case .folder:
|
|
||||||
return [currentProgram.imageSource(.primary, maxWidth: maxWidth)]
|
|
||||||
case .video:
|
|
||||||
return [currentProgram.imageSource(.primary, maxWidth: maxWidth)]
|
|
||||||
default:
|
|
||||||
return [
|
|
||||||
currentProgram.imageSource(.thumb, maxWidth: maxWidth),
|
|
||||||
currentProgram.imageSource(.backdrop, maxWidth: maxWidth),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cinematicPosterImageSources() -> [ImageSource] {
|
|
||||||
guard let currentProgram else { return [] }
|
|
||||||
switch currentProgram.type {
|
|
||||||
case .episode:
|
|
||||||
return [currentProgram.seriesImageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)]
|
|
||||||
default:
|
|
||||||
return [currentProgram.imageSource(.backdrop, maxWidth: UIScreen.main.bounds.width)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,6 +24,18 @@ protocol Poster: Displayable, Hashable, Identifiable {
|
||||||
|
|
||||||
extension Poster {
|
extension Poster {
|
||||||
|
|
||||||
|
var showTitle: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
|
||||||
|
.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
func cinematicPosterImageSources() -> [ImageSource] {
|
func cinematicPosterImageSources() -> [ImageSource] {
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,14 @@
|
||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// TODO: Rename to `PosterDisplayType` or `PosterDisplay`?
|
// TODO: Refactor to `ItemDisplayType`
|
||||||
// TODO: after no longer experimental, nest under `Poster`
|
// - this is to move away from video specific to generalizing all media types. However,
|
||||||
|
// media is still able to use grammar for their own contexts.
|
||||||
|
// - move landscape/portrait to wide/narrow
|
||||||
|
// - add `square`/something similar
|
||||||
|
// TODO: after no longer experimental, nest under `Poster`?
|
||||||
// tracker: https://github.com/apple/swift-evolution/blob/main/proposals/0404-nested-protocols.md
|
// tracker: https://github.com/apple/swift-evolution/blob/main/proposals/0404-nested-protocols.md
|
||||||
|
|
||||||
enum PosterType: String, CaseIterable, Displayable, Defaults.Serializable {
|
enum PosterType: String, CaseIterable, Displayable, Defaults.Serializable {
|
||||||
|
|
||||||
case landscape
|
case landscape
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Factory
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
final class ChannelLibraryViewModel: PagingLibraryViewModel<ChannelProgram> {
|
||||||
|
|
||||||
|
override func get(page: Int) async throws -> [ChannelProgram] {
|
||||||
|
|
||||||
|
var parameters = Paths.GetLiveTvChannelsParameters()
|
||||||
|
parameters.fields = .MinimumFields
|
||||||
|
parameters.userID = userSession.user.id
|
||||||
|
parameters.sortBy = [ItemSortBy.name.rawValue]
|
||||||
|
|
||||||
|
parameters.limit = pageSize
|
||||||
|
parameters.startIndex = page * pageSize
|
||||||
|
|
||||||
|
let request = Paths.getLiveTvChannels(parameters: parameters)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
let processedChannels = try await getPrograms(for: response.value.items ?? [])
|
||||||
|
|
||||||
|
return processedChannels
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getPrograms(for channels: [BaseItemDto]) async throws -> [ChannelProgram] {
|
||||||
|
|
||||||
|
guard let minEndDate = Calendar.current.date(byAdding: .hour, value: -1, to: .now),
|
||||||
|
let maxStartDate = Calendar.current.date(byAdding: .hour, value: 6, to: .now) else { return [] }
|
||||||
|
|
||||||
|
var parameters = Paths.GetLiveTvProgramsParameters()
|
||||||
|
parameters.channelIDs = channels.compactMap(\.id)
|
||||||
|
parameters.userID = userSession.user.id
|
||||||
|
parameters.maxStartDate = maxStartDate
|
||||||
|
parameters.minEndDate = minEndDate
|
||||||
|
parameters.sortBy = ["StartDate"]
|
||||||
|
|
||||||
|
let request = Paths.getLiveTvPrograms(parameters: parameters)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
let groupedPrograms = (response.value.items ?? [])
|
||||||
|
.grouped { program in
|
||||||
|
channels.first(where: { $0.id == program.channelID })
|
||||||
|
}
|
||||||
|
|
||||||
|
let channelPrograms: [ChannelProgram] = channels
|
||||||
|
.reduce(into: [:]) { partialResult, channel in
|
||||||
|
partialResult[channel] = (groupedPrograms[channel] ?? [])
|
||||||
|
.sorted(using: \.startDate)
|
||||||
|
}
|
||||||
|
.map(ChannelProgram.init)
|
||||||
|
.sorted(using: \.channel.name)
|
||||||
|
|
||||||
|
return channelPrograms
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,204 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Factory
|
|
||||||
import Foundation
|
|
||||||
import Get
|
|
||||||
import JellyfinAPI
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static let livePlayerDismissed = Notification.Name("livePlayerDismissed")
|
|
||||||
}
|
|
||||||
|
|
||||||
final class LiveTVChannelsViewModel: PagingLibraryViewModel<LiveTVChannelProgram> {
|
|
||||||
|
|
||||||
@Published
|
|
||||||
var channels: [BaseItemDto] = []
|
|
||||||
@Published
|
|
||||||
var channelPrograms: [LiveTVChannelProgram] = []
|
|
||||||
|
|
||||||
private var programs = [BaseItemDto]()
|
|
||||||
private var channelProgramsList = [BaseItemDto: [BaseItemDto]]()
|
|
||||||
private var timer: Timer?
|
|
||||||
|
|
||||||
var timeFormatter: DateFormatter {
|
|
||||||
let df = DateFormatter()
|
|
||||||
df.dateFormat = "h:mm"
|
|
||||||
return df
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func get(page: Int) async throws -> [LiveTVChannelProgram] {
|
|
||||||
try await getChannelPrograms()
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
stopScheduleCheckTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getChannelPrograms() async throws -> [LiveTVChannelProgram] {
|
|
||||||
let _ = try await getGuideInfo()
|
|
||||||
let channelsResponse = try await getChannels()
|
|
||||||
guard let channels = channelsResponse.value.items, !channels.isEmpty else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let programsResponse = try await getPrograms(channelIds: channels.compactMap(\.id))
|
|
||||||
let fetchedPrograms = programsResponse.value.items ?? []
|
|
||||||
await MainActor.run {
|
|
||||||
self.programs.append(contentsOf: fetchedPrograms)
|
|
||||||
}
|
|
||||||
var newChannelPrograms = [LiveTVChannelProgram]()
|
|
||||||
let now = Date()
|
|
||||||
for channel in channels {
|
|
||||||
let prgs = programs.filter { item in
|
|
||||||
item.channelID == channel.id
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentPrg: BaseItemDto?
|
|
||||||
for prg in prgs {
|
|
||||||
if let startDate = prg.startDate,
|
|
||||||
let endDate = prg.endDate,
|
|
||||||
now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate &&
|
|
||||||
now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate
|
|
||||||
{
|
|
||||||
currentPrg = prg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newChannelPrograms.append(LiveTVChannelProgram(channel: channel, currentProgram: currentPrg, programs: prgs))
|
|
||||||
}
|
|
||||||
|
|
||||||
return newChannelPrograms
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getGuideInfo() async throws -> Response<GuideInfo> {
|
|
||||||
let request = Paths.getGuideInfo
|
|
||||||
return try await userSession.client.send(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getChannels() async throws -> Response<BaseItemDtoQueryResult> {
|
|
||||||
let parameters = Paths.GetLiveTvChannelsParameters(
|
|
||||||
userID: userSession.user.id,
|
|
||||||
startIndex: currentPage * pageSize,
|
|
||||||
limit: pageSize,
|
|
||||||
enableImageTypes: [.primary],
|
|
||||||
fields: ItemFields.MinimumFields,
|
|
||||||
enableUserData: false,
|
|
||||||
enableFavoriteSorting: true
|
|
||||||
)
|
|
||||||
let request = Paths.getLiveTvChannels(parameters: parameters)
|
|
||||||
return try await userSession.client.send(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getPrograms(channelIds: [String]) async throws -> Response<BaseItemDtoQueryResult> {
|
|
||||||
let minEndDate = Date.now.addComponentsToDate(hours: -1)
|
|
||||||
let maxStartDate = minEndDate.addComponentsToDate(hours: 6)
|
|
||||||
let parameters = Paths.GetLiveTvProgramsParameters(
|
|
||||||
channelIDs: channelIds,
|
|
||||||
userID: userSession.user.id,
|
|
||||||
maxStartDate: maxStartDate,
|
|
||||||
minEndDate: minEndDate,
|
|
||||||
sortBy: ["StartDate"]
|
|
||||||
)
|
|
||||||
let request = Paths.getLiveTvPrograms(parameters: parameters)
|
|
||||||
return try await userSession.client.send(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startScheduleCheckTimer() {
|
|
||||||
let date = Date()
|
|
||||||
let calendar = Calendar.current
|
|
||||||
var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute], from: date)
|
|
||||||
// Run every minute
|
|
||||||
guard let minute = components.minute else { return }
|
|
||||||
components.second = 0
|
|
||||||
components.minute = minute + (1 - (minute % 1))
|
|
||||||
guard let nextMinute = calendar.date(from: components) else { return }
|
|
||||||
if let existingTimer = timer {
|
|
||||||
existingTimer.invalidate()
|
|
||||||
}
|
|
||||||
timer = Timer(fire: nextMinute, interval: 60, repeats: true) { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.logger.debug("LiveTVChannels schedule check...")
|
|
||||||
|
|
||||||
Task {
|
|
||||||
await MainActor.run {
|
|
||||||
let channelProgramsCopy = self.channelPrograms
|
|
||||||
var refreshedChannelPrograms: [LiveTVChannelProgram] = []
|
|
||||||
for channelProgram in channelProgramsCopy {
|
|
||||||
var currentPrg: BaseItemDto?
|
|
||||||
let now = Date()
|
|
||||||
for prg in channelProgram.programs {
|
|
||||||
if let startDate = prg.startDate,
|
|
||||||
let endDate = prg.endDate,
|
|
||||||
now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate &&
|
|
||||||
now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate
|
|
||||||
{
|
|
||||||
currentPrg = prg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshedChannelPrograms
|
|
||||||
.append(LiveTVChannelProgram(
|
|
||||||
channel: channelProgram.channel,
|
|
||||||
currentProgram: currentPrg,
|
|
||||||
programs: channelProgram.programs
|
|
||||||
))
|
|
||||||
}
|
|
||||||
self.channelPrograms = refreshedChannelPrograms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let timer = timer {
|
|
||||||
RunLoop.main.add(timer, forMode: .default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopScheduleCheckTimer() {
|
|
||||||
timer?.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Array {
|
|
||||||
func chunked(into size: Int) -> [[Element]] {
|
|
||||||
stride(from: 0, to: count, by: size).map {
|
|
||||||
Array(self[$0 ..< Swift.min($0 + size, count)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Date {
|
|
||||||
func addComponentsToDate(seconds sec: Int? = nil, minutes min: Int? = nil, hours hrs: Int? = nil, days d: Int? = nil) -> Date {
|
|
||||||
var dc = DateComponents()
|
|
||||||
if let sec = sec {
|
|
||||||
dc.second = sec
|
|
||||||
}
|
|
||||||
if let min = min {
|
|
||||||
dc.minute = min
|
|
||||||
}
|
|
||||||
if let hrs = hrs {
|
|
||||||
dc.hour = hrs
|
|
||||||
}
|
|
||||||
if let d = d {
|
|
||||||
dc.day = d
|
|
||||||
}
|
|
||||||
return Calendar.current.date(byAdding: dc, to: self)!
|
|
||||||
}
|
|
||||||
|
|
||||||
func midnightUTCDate() -> Date {
|
|
||||||
var dc: DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: self)
|
|
||||||
dc.hour = 0
|
|
||||||
dc.minute = 0
|
|
||||||
dc.second = 0
|
|
||||||
dc.nanosecond = 0
|
|
||||||
dc.timeZone = TimeZone(secondsFromGMT: 0)
|
|
||||||
return Calendar.current.date(from: dc)!
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
|
||||||
|
|
||||||
final class LiveTVProgramsViewModel: ViewModel {
|
|
||||||
|
|
||||||
@Published
|
|
||||||
var recommendedItems = [BaseItemDto]()
|
|
||||||
@Published
|
|
||||||
var seriesItems = [BaseItemDto]()
|
|
||||||
@Published
|
|
||||||
var movieItems = [BaseItemDto]()
|
|
||||||
@Published
|
|
||||||
var sportsItems = [BaseItemDto]()
|
|
||||||
@Published
|
|
||||||
var kidsItems = [BaseItemDto]()
|
|
||||||
@Published
|
|
||||||
var newsItems = [BaseItemDto]()
|
|
||||||
|
|
||||||
var channels = [String: BaseItemDto]()
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
// getChannels()
|
|
||||||
}
|
|
||||||
|
|
||||||
func findChannel(id: String) -> BaseItemDto? {
|
|
||||||
channels[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getChannels() {
|
|
||||||
Task {
|
|
||||||
let parameters = Paths.GetLiveTvChannelsParameters(
|
|
||||||
userID: userSession.user.id,
|
|
||||||
startIndex: 0,
|
|
||||||
limit: 1000,
|
|
||||||
enableImageTypes: [.primary],
|
|
||||||
enableUserData: false,
|
|
||||||
enableFavoriteSorting: true
|
|
||||||
)
|
|
||||||
let request = Paths.getLiveTvChannels(parameters: parameters)
|
|
||||||
let response = try await userSession.client.send(request)
|
|
||||||
|
|
||||||
guard let channels = response.value.items else { return }
|
|
||||||
|
|
||||||
for channel in channels {
|
|
||||||
guard let channelID = channel.id else { continue }
|
|
||||||
self.channels[channelID] = channel
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecommendedPrograms()
|
|
||||||
getSeries()
|
|
||||||
getMovies()
|
|
||||||
getSports()
|
|
||||||
getKids()
|
|
||||||
getNews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getRecommendedPrograms() {
|
|
||||||
Task {
|
|
||||||
let parameters = Paths.GetRecommendedProgramsParameters(
|
|
||||||
userID: userSession.user.id,
|
|
||||||
limit: 9,
|
|
||||||
isAiring: true,
|
|
||||||
imageTypeLimit: 1,
|
|
||||||
enableImageTypes: [.primary, .thumb],
|
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio],
|
|
||||||
enableTotalRecordCount: false
|
|
||||||
)
|
|
||||||
let request = Paths.getRecommendedPrograms(parameters: parameters)
|
|
||||||
let response = try await userSession.client.send(request)
|
|
||||||
|
|
||||||
guard let items = response.value.items else { return }
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self.recommendedItems = items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getSeries() {
|
|
||||||
Task {
|
|
||||||
let request = Paths.getPrograms(.init(
|
|
||||||
enableImageTypes: [.primary, .thumb],
|
|
||||||
enableTotalRecordCount: false,
|
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio],
|
|
||||||
hasAired: false,
|
|
||||||
isKids: false,
|
|
||||||
isMovie: false,
|
|
||||||
isNews: false,
|
|
||||||
isSeries: true,
|
|
||||||
isSports: false,
|
|
||||||
limit: 9,
|
|
||||||
userID: userSession.user.id
|
|
||||||
))
|
|
||||||
let response = try await userSession.client.send(request)
|
|
||||||
|
|
||||||
guard let items = response.value.items else { return }
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self.seriesItems = items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getMovies() {
|
|
||||||
Task {
|
|
||||||
let request = Paths.getPrograms(.init(
|
|
||||||
enableImageTypes: [.primary, .thumb],
|
|
||||||
enableTotalRecordCount: false,
|
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio],
|
|
||||||
hasAired: false,
|
|
||||||
isKids: false,
|
|
||||||
isMovie: true,
|
|
||||||
isNews: false,
|
|
||||||
isSeries: false,
|
|
||||||
isSports: false,
|
|
||||||
limit: 9,
|
|
||||||
userID: userSession.user.id
|
|
||||||
))
|
|
||||||
let response = try await userSession.client.send(request)
|
|
||||||
|
|
||||||
guard let items = response.value.items else { return }
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self.movieItems = items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getSports() {
|
|
||||||
Task {
|
|
||||||
let request = Paths.getPrograms(.init(
|
|
||||||
enableImageTypes: [.primary, .thumb],
|
|
||||||
enableTotalRecordCount: false,
|
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio],
|
|
||||||
hasAired: false,
|
|
||||||
isKids: false,
|
|
||||||
isMovie: false,
|
|
||||||
isNews: false,
|
|
||||||
isSeries: false,
|
|
||||||
isSports: true,
|
|
||||||
limit: 9,
|
|
||||||
userID: userSession.user.id
|
|
||||||
))
|
|
||||||
let response = try await userSession.client.send(request)
|
|
||||||
|
|
||||||
guard let items = response.value.items else { return }
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self.sportsItems = items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getKids() {
|
|
||||||
Task {
|
|
||||||
let request = Paths.getPrograms(.init(
|
|
||||||
enableImageTypes: [.primary, .thumb],
|
|
||||||
enableTotalRecordCount: false,
|
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio],
|
|
||||||
hasAired: false,
|
|
||||||
isKids: true,
|
|
||||||
isMovie: false,
|
|
||||||
isNews: false,
|
|
||||||
isSeries: false,
|
|
||||||
isSports: false,
|
|
||||||
limit: 9,
|
|
||||||
userID: userSession.user.id
|
|
||||||
))
|
|
||||||
let response = try await userSession.client.send(request)
|
|
||||||
|
|
||||||
guard let items = response.value.items else { return }
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self.kidsItems = items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getNews() {
|
|
||||||
Task {
|
|
||||||
let request = Paths.getPrograms(.init(
|
|
||||||
enableImageTypes: [.primary, .thumb],
|
|
||||||
enableTotalRecordCount: false,
|
|
||||||
fields: [.channelInfo, .primaryImageAspectRatio],
|
|
||||||
hasAired: false,
|
|
||||||
isKids: false,
|
|
||||||
isMovie: false,
|
|
||||||
isNews: true,
|
|
||||||
isSeries: false,
|
|
||||||
isSports: false,
|
|
||||||
limit: 9,
|
|
||||||
userID: userSession.user.id
|
|
||||||
))
|
|
||||||
let response = try await userSession.client.send(request)
|
|
||||||
|
|
||||||
guard let items = response.value.items else { return }
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
self.seriesItems = items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,11 +12,9 @@ import JellyfinAPI
|
||||||
class LiveVideoPlayerManager: VideoPlayerManager {
|
class LiveVideoPlayerManager: VideoPlayerManager {
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var program: LiveTVChannelProgram?
|
var program: ChannelProgram?
|
||||||
@Published
|
|
||||||
var dateFormatter = DateFormatter()
|
|
||||||
|
|
||||||
init(item: BaseItemDto, mediaSource: MediaSourceInfo, program: LiveTVChannelProgram? = nil) {
|
init(item: BaseItemDto, mediaSource: MediaSourceInfo, program: ChannelProgram? = nil) {
|
||||||
self.program = program
|
self.program = program
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
// TODO: is current program-channel requesting best way to do it?
|
||||||
|
|
||||||
|
// Note: section item limit is low so that total channel amount is not too much
|
||||||
|
|
||||||
|
final class ProgramsViewModel: ViewModel, Stateful {
|
||||||
|
|
||||||
|
enum ProgramSection: CaseIterable {
|
||||||
|
case kids
|
||||||
|
case movies
|
||||||
|
case news
|
||||||
|
case recommended
|
||||||
|
case series
|
||||||
|
case sports
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Action
|
||||||
|
|
||||||
|
enum Action: Equatable {
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
case refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: State
|
||||||
|
|
||||||
|
enum State: Hashable {
|
||||||
|
case content
|
||||||
|
case error(JellyfinAPIError)
|
||||||
|
case initial
|
||||||
|
case refreshing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
private(set) var kids: [ChannelProgram] = []
|
||||||
|
@Published
|
||||||
|
private(set) var movies: [ChannelProgram] = []
|
||||||
|
@Published
|
||||||
|
private(set) var news: [ChannelProgram] = []
|
||||||
|
@Published
|
||||||
|
private(set) var recommended: [ChannelProgram] = []
|
||||||
|
@Published
|
||||||
|
private(set) var series: [ChannelProgram] = []
|
||||||
|
@Published
|
||||||
|
private(set) var sports: [ChannelProgram] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
final var lastAction: Action? = nil
|
||||||
|
@Published
|
||||||
|
final var state: State = .initial
|
||||||
|
|
||||||
|
private var programChannels: [BaseItemDto] = []
|
||||||
|
|
||||||
|
private var currentRefreshTask: AnyCancellable?
|
||||||
|
|
||||||
|
var hasNoResults: Bool {
|
||||||
|
kids.isEmpty &&
|
||||||
|
movies.isEmpty &&
|
||||||
|
news.isEmpty &&
|
||||||
|
recommended.isEmpty &&
|
||||||
|
series.isEmpty &&
|
||||||
|
sports.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func respond(to action: Action) -> State {
|
||||||
|
switch action {
|
||||||
|
case let .error(error):
|
||||||
|
return .error(error)
|
||||||
|
case .refresh:
|
||||||
|
currentRefreshTask?.cancel()
|
||||||
|
|
||||||
|
currentRefreshTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let sections = try await getItemSections()
|
||||||
|
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.kids = sections[.kids] ?? []
|
||||||
|
self.movies = sections[.movies] ?? []
|
||||||
|
self.news = sections[.news] ?? []
|
||||||
|
self.recommended = sections[.recommended] ?? []
|
||||||
|
self.series = sections[.series] ?? []
|
||||||
|
self.sports = sections[.sports] ?? []
|
||||||
|
|
||||||
|
self.state = .content
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.send(.error(.init(error.localizedDescription)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asAnyCancellable()
|
||||||
|
|
||||||
|
return .refreshing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getItemSections() async throws -> [ProgramSection: [ChannelProgram]] {
|
||||||
|
try await withThrowingTaskGroup(
|
||||||
|
of: (ProgramSection, [BaseItemDto]).self,
|
||||||
|
returning: [ProgramSection: [ChannelProgram]].self
|
||||||
|
) { group in
|
||||||
|
|
||||||
|
// sections
|
||||||
|
for section in ProgramSection.allCases {
|
||||||
|
group.addTask {
|
||||||
|
let items = try await self.getPrograms(for: section)
|
||||||
|
return (section, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recommended
|
||||||
|
group.addTask {
|
||||||
|
let items = try await self.getRecommendedPrograms()
|
||||||
|
return (ProgramSection.recommended, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
var programs: [ProgramSection: [BaseItemDto]] = [:]
|
||||||
|
|
||||||
|
while let items = try await group.next() {
|
||||||
|
programs[items.0] = items.1
|
||||||
|
}
|
||||||
|
|
||||||
|
// get channels for all programs at once to
|
||||||
|
// avoid going back and forth too much
|
||||||
|
let channels = try await Set(self.getChannels(for: programs.values.flatMap { $0 }))
|
||||||
|
|
||||||
|
let result: [ProgramSection: [ChannelProgram]] = programs.mapValues { programs in
|
||||||
|
programs.compactMap { program in
|
||||||
|
guard let channel = channels.first(where: { channel in channel.id == program.channelID }) else { return nil }
|
||||||
|
return ChannelProgram(channel: channel, programs: [program])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getRecommendedPrograms() async throws -> [BaseItemDto] {
|
||||||
|
|
||||||
|
var parameters = Paths.GetRecommendedProgramsParameters()
|
||||||
|
parameters.fields = .MinimumFields
|
||||||
|
.appending(.channelInfo)
|
||||||
|
parameters.isAiring = true
|
||||||
|
parameters.limit = 10
|
||||||
|
parameters.userID = userSession.user.id
|
||||||
|
|
||||||
|
let request = Paths.getRecommendedPrograms(parameters: parameters)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return response.value.items ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getPrograms(for section: ProgramSection) async throws -> [BaseItemDto] {
|
||||||
|
|
||||||
|
var parameters = Paths.GetLiveTvProgramsParameters()
|
||||||
|
parameters.fields = .MinimumFields
|
||||||
|
.appending(.channelInfo)
|
||||||
|
.appending(.mediaSources)
|
||||||
|
parameters.hasAired = false
|
||||||
|
parameters.limit = 10
|
||||||
|
parameters.userID = userSession.user.id
|
||||||
|
|
||||||
|
parameters.isKids = section == .kids
|
||||||
|
parameters.isMovie = section == .movies
|
||||||
|
parameters.isNews = section == .news
|
||||||
|
parameters.isSeries = section == .series
|
||||||
|
parameters.isSports = section == .sports
|
||||||
|
|
||||||
|
let request = Paths.getLiveTvPrograms(parameters: parameters)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return response.value.items ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getChannels(for programs: [BaseItemDto]) async throws -> [BaseItemDto] {
|
||||||
|
|
||||||
|
var parameters = Paths.GetItemsByUserIDParameters()
|
||||||
|
parameters.fields = .MinimumFields
|
||||||
|
parameters.ids = programs.compactMap(\.channelID)
|
||||||
|
|
||||||
|
let request = Paths.getItemsByUserID(
|
||||||
|
userID: userSession.user.id,
|
||||||
|
parameters: parameters
|
||||||
|
)
|
||||||
|
let response = try await userSession.client.send(request)
|
||||||
|
|
||||||
|
return response.value.items ?? []
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import VLCUI
|
||||||
// TODO: should view models handle progress reports instead, with a protocol
|
// TODO: should view models handle progress reports instead, with a protocol
|
||||||
// for other types of media handling
|
// for other types of media handling
|
||||||
|
|
||||||
|
// TODO: transition to `Stateful`
|
||||||
class VideoPlayerManager: ViewModel {
|
class VideoPlayerManager: ViewModel {
|
||||||
|
|
||||||
class CurrentProgressHandler: ObservableObject {
|
class CurrentProgressHandler: ObservableObject {
|
||||||
|
|
|
@ -64,6 +64,7 @@ class VideoPlayerViewModel: ViewModel {
|
||||||
return hlsStreamComponents.url!
|
return hlsStreamComponents.url!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: should start time be from the media source instead?
|
||||||
var vlcVideoPlayerConfiguration: VLCVideoPlayer.Configuration {
|
var vlcVideoPlayerConfiguration: VLCVideoPlayer.Configuration {
|
||||||
let configuration = VLCVideoPlayer.Configuration(url: playbackURL)
|
let configuration = VLCVideoPlayer.Configuration(url: playbackURL)
|
||||||
configuration.autoPlay = true
|
configuration.autoPlay = true
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import JellyfinAPI
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct CutOffShadow: Shape {
|
|
||||||
func path(in rect: CGRect) -> Path {
|
|
||||||
var path = Path()
|
|
||||||
|
|
||||||
let tl = CGPoint(x: rect.minX, y: rect.minY)
|
|
||||||
let tr = CGPoint(x: rect.maxX, y: rect.minY)
|
|
||||||
let brs = CGPoint(x: rect.maxX, y: rect.maxY - 6)
|
|
||||||
let brc = CGPoint(x: rect.maxX - 6, y: rect.maxY - 6)
|
|
||||||
let bls = CGPoint(x: rect.minX + 6, y: rect.maxY)
|
|
||||||
let blc = CGPoint(x: rect.minX + 6, y: rect.maxY - 6)
|
|
||||||
|
|
||||||
path.move(to: tl)
|
|
||||||
path.addLine(to: tr)
|
|
||||||
path.addLine(to: brs)
|
|
||||||
path.addRelativeArc(
|
|
||||||
center: brc,
|
|
||||||
radius: 6,
|
|
||||||
startAngle: Angle.degrees(0),
|
|
||||||
delta: Angle.degrees(90)
|
|
||||||
)
|
|
||||||
path.addLine(to: bls)
|
|
||||||
path.addRelativeArc(
|
|
||||||
center: blc,
|
|
||||||
radius: 6,
|
|
||||||
startAngle: Angle.degrees(90),
|
|
||||||
delta: Angle.degrees(90)
|
|
||||||
)
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LandscapeItemElement: View {
|
|
||||||
@Environment(\.isFocused)
|
|
||||||
var envFocused: Bool
|
|
||||||
@State
|
|
||||||
var focused: Bool = false
|
|
||||||
@State
|
|
||||||
var backgroundURL: URL?
|
|
||||||
|
|
||||||
var item: BaseItemDto
|
|
||||||
var inSeasonView: Bool?
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
ImageView(item.imageSource(.backdrop, maxWidth: 445))
|
|
||||||
.frame(width: 445, height: 250)
|
|
||||||
.cornerRadius(10)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.overlay(
|
|
||||||
ZStack {
|
|
||||||
if item.userData?.isPlayed ?? false {
|
|
||||||
Image(systemName: "circle.fill")
|
|
||||||
.foregroundColor(.white)
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundColor(Color(.systemBlue))
|
|
||||||
}
|
|
||||||
}.padding(2)
|
|
||||||
.opacity(1),
|
|
||||||
alignment: .topTrailing
|
|
||||||
).opacity(1)
|
|
||||||
.overlay(ZStack(alignment: .leading) {
|
|
||||||
if focused && item.userData?.playedPercentage != nil {
|
|
||||||
Rectangle()
|
|
||||||
.fill(LinearGradient(
|
|
||||||
gradient: Gradient(colors: [.black, .clear]),
|
|
||||||
startPoint: .bottom,
|
|
||||||
endPoint: .top
|
|
||||||
))
|
|
||||||
.frame(width: 445, height: 90)
|
|
||||||
.mask(CutOffShadow())
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("CONTINUE • \(item.progressLabel ?? "")")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.offset(y: 5)
|
|
||||||
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(item.userData?.playedPercentage ?? 0 * 4.45 - 0.16), height: 12)
|
|
||||||
}
|
|
||||||
}.padding(12)
|
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}, alignment: .bottomLeading)
|
|
||||||
.shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0)
|
|
||||||
.shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0)
|
|
||||||
if inSeasonView ?? false {
|
|
||||||
Text("\(item.episodeLocator ?? "") • \(item.name ?? "")")
|
|
||||||
.font(.callout)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.lineLimit(1)
|
|
||||||
.frame(width: 445)
|
|
||||||
} else {
|
|
||||||
Text(item.type == .episode ? "\(item.seriesName ?? "") • \(item.episodeLocator ?? "")" : item.name ?? "")
|
|
||||||
.font(.callout)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.lineLimit(1)
|
|
||||||
.frame(width: 445)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: envFocused) { envFocus in
|
|
||||||
withAnimation(.linear(duration: 0.15)) {
|
|
||||||
self.focused = envFocus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scaleEffect(focused ? 1.1 : 1)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,8 +10,13 @@ import SwiftUI
|
||||||
|
|
||||||
struct LandscapePosterProgressBar: View {
|
struct LandscapePosterProgressBar: View {
|
||||||
|
|
||||||
let title: String
|
private let title: String?
|
||||||
let progress: CGFloat
|
private let progress: Double
|
||||||
|
|
||||||
|
init(title: String? = nil, progress: Double) {
|
||||||
|
self.title = title
|
||||||
|
self.progress = progress
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
|
@ -26,15 +31,16 @@ struct LandscapePosterProgressBar: View {
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
|
||||||
Text(title)
|
if let title {
|
||||||
.font(.subheadline)
|
Text(title)
|
||||||
.foregroundColor(.white)
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
|
||||||
ProgressBar(progress: progress)
|
ProgressBar(progress: progress)
|
||||||
.frame(height: 5)
|
.frame(height: 5)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 5)
|
.padding(10)
|
||||||
.padding(.bottom, 7)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 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 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 {
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
Text(channel.number ?? "")
|
|
||||||
.font(.footnote)
|
|
||||||
.frame(alignment: .leading)
|
|
||||||
.padding()
|
|
||||||
Spacer()
|
|
||||||
}.frame(alignment: .top)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
GeometryReader { gp in
|
|
||||||
VStack {
|
|
||||||
ImageView(channel.imageSource(.primary, 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))
|
|
||||||
|
|
||||||
programLabel(
|
|
||||||
timeText: currentProgramText.timeDisplay,
|
|
||||||
titleText: currentProgramText.title,
|
|
||||||
color: Color.primary,
|
|
||||||
font: Font.system(size: 20, weight: .bold, design: .default)
|
|
||||||
)
|
|
||||||
if nextProgramsText.isNotEmpty {
|
|
||||||
let nextItem = nextProgramsText[0]
|
|
||||||
programLabel(
|
|
||||||
timeText: nextItem.timeDisplay,
|
|
||||||
titleText: nextItem.title,
|
|
||||||
color: Color.gray,
|
|
||||||
font: Font.system(size: 20, design: .default)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if nextProgramsText.count > 1 {
|
|
||||||
let nextItem2 = nextProgramsText[1]
|
|
||||||
programLabel(
|
|
||||||
timeText: nextItem2.timeDisplay,
|
|
||||||
titleText: nextItem2.title,
|
|
||||||
color: Color.gray,
|
|
||||||
font: Font.system(size: 20, design: .default)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
|
||||||
.padding(.init(top: gp.size.height / 2, leading: 16, bottom: 56, trailing: 16))
|
|
||||||
.opacity(loading ? 0.5 : 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if loading {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
GeometryReader { gp in
|
|
||||||
ZStack(alignment: .leading) {
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.fill(Color.gray)
|
|
||||||
.opacity(0.4)
|
|
||||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 8, maxHeight: 8)
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.fill(Color.jellyfinPurple)
|
|
||||||
.frame(width: CGFloat(progressPercent * gp.size.width), height: 8)
|
|
||||||
}
|
|
||||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
|
||||||
.padding(.init(top: 0, leading: 16, bottom: 32, trailing: 16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(minWidth: 400, minHeight: 400)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 20)
|
|
||||||
.stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4)
|
|
||||||
)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.scaleEffect(isFocused ? 1.1 : 1)
|
|
||||||
.focusable(true)
|
|
||||||
.focused($focused)
|
|
||||||
.onChange(of: focused) { foc in
|
|
||||||
withAnimation(.linear(duration: 0.15)) {
|
|
||||||
self.isFocused = foc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onLongPressGesture(minimumDuration: 0.01, pressing: { _ in }) {
|
|
||||||
onSelect { loadingState in
|
|
||||||
loading = loadingState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import CollectionVGrid
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChannelLibraryView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: VideoPlayerWrapperCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel = ChannelLibraryViewModel()
|
||||||
|
|
||||||
|
private var contentView: some View {
|
||||||
|
CollectionVGrid(
|
||||||
|
$viewModel.elements,
|
||||||
|
layout: .columns(3, insets: .init(0), itemSpacing: 25, lineSpacing: 25)
|
||||||
|
) { channel in
|
||||||
|
WideChannelGridItem(channel: channel)
|
||||||
|
.onSelect {
|
||||||
|
guard let mediaSource = channel.channel.mediaSources?.first else { return }
|
||||||
|
router.route(
|
||||||
|
to: \.liveVideoPlayer,
|
||||||
|
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReachedBottomEdge(offset: .offset(300)) {
|
||||||
|
viewModel.send(.getNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WrappedView {
|
||||||
|
switch viewModel.state {
|
||||||
|
case .content:
|
||||||
|
if viewModel.elements.isEmpty {
|
||||||
|
L10n.noResults.text
|
||||||
|
} else {
|
||||||
|
contentView
|
||||||
|
}
|
||||||
|
case let .error(error):
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
case .initial, .refreshing:
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onFirstAppear {
|
||||||
|
if viewModel.state == .initial {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.afterLastDisappear { interval in
|
||||||
|
// refresh after 3 hours
|
||||||
|
if interval >= 10800 {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ChannelLibraryView {
|
||||||
|
|
||||||
|
struct WideChannelGridItem: View {
|
||||||
|
|
||||||
|
@Default(.accentColor)
|
||||||
|
private var accentColor
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var now: Date = .now
|
||||||
|
|
||||||
|
let channel: ChannelProgram
|
||||||
|
|
||||||
|
private var onSelect: () -> Void
|
||||||
|
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
private var channelLogo: some View {
|
||||||
|
VStack {
|
||||||
|
ZStack {
|
||||||
|
Color.clear
|
||||||
|
|
||||||
|
ImageView(channel.portraitPosterImageSource(maxWidth: 110))
|
||||||
|
.image {
|
||||||
|
$0.aspectRatio(contentMode: .fit)
|
||||||
|
}
|
||||||
|
.failure {
|
||||||
|
SystemImageContentView(systemName: channel.typeSystemImage)
|
||||||
|
.background(color: .clear)
|
||||||
|
.imageFrameRatio(width: 1.5, height: 1.5)
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.aspectRatio(1.0, contentMode: .fit)
|
||||||
|
|
||||||
|
Text(channel.channel.number ?? "")
|
||||||
|
.font(.body)
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func programLabel(for program: BaseItemDto) -> some View {
|
||||||
|
HStack(alignment: .top, spacing: EdgeInsets.defaultEdgePadding / 2) {
|
||||||
|
AlternateLayoutView(alignment: .leading) {
|
||||||
|
Text("00:00 AM")
|
||||||
|
.monospacedDigit()
|
||||||
|
} content: {
|
||||||
|
if let startDate = program.startDate {
|
||||||
|
Text(startDate, style: .time)
|
||||||
|
.monospacedDigit()
|
||||||
|
} else {
|
||||||
|
Text(String.emptyTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(program.displayTitle)
|
||||||
|
}
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var programListView: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
if let currentProgram = channel.currentProgram {
|
||||||
|
ProgressBar(progress: currentProgram.programProgress(relativeTo: now) ?? 0)
|
||||||
|
.frame(height: 8)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
|
||||||
|
programLabel(for: currentProgram)
|
||||||
|
.font(.caption.weight(.bold))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let nextProgram = channel.programAfterCurrent(offset: 0) {
|
||||||
|
programLabel(for: nextProgram)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let futureProgram = channel.programAfterCurrent(offset: 1) {
|
||||||
|
programLabel(for: futureProgram)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.id(channel.currentProgram)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
onSelect()
|
||||||
|
} label: {
|
||||||
|
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding / 2) {
|
||||||
|
|
||||||
|
channelLogo
|
||||||
|
.frame(width: 110)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text(channel.displayTitle)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
if channel.programs.isNotEmpty {
|
||||||
|
programListView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.padding(.horizontal, EdgeInsets.defaultEdgePadding / 2)
|
||||||
|
}
|
||||||
|
.buttonStyle(.card)
|
||||||
|
.frame(height: 200)
|
||||||
|
.onReceive(timer) { newValue in
|
||||||
|
now = newValue
|
||||||
|
}
|
||||||
|
.animation(.linear(duration: 0.2), value: channel.currentProgram)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChannelLibraryView.WideChannelGridItem {
|
||||||
|
|
||||||
|
init(channel: ChannelProgram) {
|
||||||
|
self.init(
|
||||||
|
channel: channel,
|
||||||
|
onSelect: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onSelect(_ action: @escaping () -> Void) -> Self {
|
||||||
|
copy(modifying: \.onSelect, with: action)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,161 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import CollectionVGrid
|
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
|
|
||||||
|
|
||||||
struct LiveTVChannelsView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var router: LiveTVChannelsCoordinator.Router
|
|
||||||
|
|
||||||
@StateObject
|
|
||||||
var viewModel = LiveTVChannelsViewModel()
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var loadingView: some View {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add retry
|
|
||||||
@ViewBuilder
|
|
||||||
private var noResultsView: some View {
|
|
||||||
L10n.noResults.text
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var channelsView: some View {
|
|
||||||
Group {
|
|
||||||
if viewModel.isLoading {
|
|
||||||
ProgressView()
|
|
||||||
} else if viewModel.elements.isNotEmpty {
|
|
||||||
CollectionVGrid(
|
|
||||||
$viewModel.elements,
|
|
||||||
layout: .minWidth(400, itemSpacing: 16, lineSpacing: 4)
|
|
||||||
) { program in
|
|
||||||
channelCell(for: program)
|
|
||||||
}
|
|
||||||
.onReachedBottomEdge(offset: .offset(300)) {
|
|
||||||
viewModel.send(.getNextPage)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
viewModel.startScheduleCheckTimer()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
viewModel.stopScheduleCheckTimer()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
VStack {
|
|
||||||
Text(L10n.noResults)
|
|
||||||
Button {
|
|
||||||
viewModel.send(.refresh)
|
|
||||||
} label: {
|
|
||||||
Text(L10n.reload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ignoresSafeArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func channelCell(for 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
|
|
||||||
}
|
|
||||||
|
|
||||||
LiveTVChannelItemElement(
|
|
||||||
channel: channel,
|
|
||||||
currentProgram: channelProgram.currentProgram,
|
|
||||||
currentProgramText: currentProgramDisplayText,
|
|
||||||
nextProgramsText: nextProgramsDisplayText(
|
|
||||||
nextItems: nextItems,
|
|
||||||
timeFormatter: viewModel.timeFormatter
|
|
||||||
),
|
|
||||||
onSelect: { _ in
|
|
||||||
guard let mediaSource = channel.mediaSources?.first else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModel.stopScheduleCheckTimer()
|
|
||||||
router.route(
|
|
||||||
to: \.liveVideoPlayer,
|
|
||||||
LiveVideoPlayerManager(item: channel, mediaSource: mediaSource, program: channelProgram)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
if viewModel.isLoading && viewModel.elements.isEmpty {
|
|
||||||
loadingView
|
|
||||||
} else if viewModel.elements.isEmpty {
|
|
||||||
noResultsView
|
|
||||||
} else {
|
|
||||||
channelsView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFirstAppear {
|
|
||||||
if viewModel.state == .initial {
|
|
||||||
viewModel.send(.refresh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func nextProgramsDisplayText(nextItems: [BaseItemDto], timeFormatter: DateFormatter) -> [LiveTVChannelViewProgram] {
|
|
||||||
var programsDisplayText: [LiveTVChannelViewProgram] = []
|
|
||||||
for item in nextItems {
|
|
||||||
programsDisplayText.append(item.programDisplayText(timeFormatter: timeFormatter))
|
|
||||||
}
|
|
||||||
return programsDisplayText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct LiveTVHomeView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
var mainCoordinator: MainCoordinator.Router
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button("Return Home")
|
|
||||||
.onAppear {
|
|
||||||
mainCoordinator.root(\.mainTab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import CollectionView
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct LiveTVProgramsView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var router: LiveTVProgramsCoordinator.Router
|
|
||||||
|
|
||||||
@StateObject
|
|
||||||
var viewModel: LiveTVProgramsViewModel
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(alignment: .leading) {
|
|
||||||
if viewModel.recommendedItems.isNotEmpty {
|
|
||||||
let items = viewModel.recommendedItems
|
|
||||||
Text("On Now")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.padding(.leading, 90)
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack {
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
ForEach(items, id: \.id) { item in
|
|
||||||
Button {
|
|
||||||
guard let channelID = item.channelID,
|
|
||||||
let channel = viewModel.findChannel(id: channelID),
|
|
||||||
let mediaSource = channel.mediaSources?.first else { return }
|
|
||||||
|
|
||||||
router.route(to: \.videoPlayer, LiveVideoPlayerManager(item: channel, mediaSource: mediaSource))
|
|
||||||
} label: {
|
|
||||||
LandscapeItemElement(item: item)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
|
||||||
}
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
}
|
|
||||||
}.frame(height: 350)
|
|
||||||
}
|
|
||||||
if viewModel.seriesItems.isNotEmpty {
|
|
||||||
let items = viewModel.seriesItems
|
|
||||||
Text("Shows")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.padding(.leading, 90)
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack {
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
ForEach(items, id: \.id) { item in
|
|
||||||
Button {
|
|
||||||
guard let channelID = item.channelID,
|
|
||||||
let channel = viewModel.findChannel(id: channelID),
|
|
||||||
let mediaSource = channel.mediaSources?.first else { return }
|
|
||||||
|
|
||||||
router.route(to: \.videoPlayer, LiveVideoPlayerManager(item: channel, mediaSource: mediaSource))
|
|
||||||
} label: {
|
|
||||||
LandscapeItemElement(item: item)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
|
||||||
}
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
}
|
|
||||||
}.frame(height: 350)
|
|
||||||
}
|
|
||||||
if viewModel.movieItems.isNotEmpty {
|
|
||||||
let items = viewModel.movieItems
|
|
||||||
Text("Movies")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.padding(.leading, 90)
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack {
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
ForEach(items, id: \.id) { item in
|
|
||||||
Button {
|
|
||||||
guard let channelID = item.channelID,
|
|
||||||
let channel = viewModel.findChannel(id: channelID),
|
|
||||||
let mediaSource = channel.mediaSources?.first else { return }
|
|
||||||
|
|
||||||
router.route(to: \.videoPlayer, LiveVideoPlayerManager(item: channel, mediaSource: mediaSource))
|
|
||||||
} label: {
|
|
||||||
LandscapeItemElement(item: item)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
|
||||||
}
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
}
|
|
||||||
}.frame(height: 350)
|
|
||||||
}
|
|
||||||
if viewModel.sportsItems.isNotEmpty {
|
|
||||||
let items = viewModel.sportsItems
|
|
||||||
Text("Sports")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.padding(.leading, 90)
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack {
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
ForEach(items, id: \.id) { item in
|
|
||||||
Button {
|
|
||||||
guard let channelID = item.channelID,
|
|
||||||
let channel = viewModel.findChannel(id: channelID),
|
|
||||||
let mediaSource = channel.mediaSources?.first else { return }
|
|
||||||
|
|
||||||
router.route(to: \.videoPlayer, LiveVideoPlayerManager(item: channel, mediaSource: mediaSource))
|
|
||||||
} label: {
|
|
||||||
LandscapeItemElement(item: item)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
|
||||||
}
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
}
|
|
||||||
}.frame(height: 350)
|
|
||||||
}
|
|
||||||
if viewModel.kidsItems.isNotEmpty {
|
|
||||||
let items = viewModel.kidsItems
|
|
||||||
Text("Kids")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.padding(.leading, 90)
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack {
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
ForEach(items, id: \.id) { item in
|
|
||||||
Button {
|
|
||||||
guard let channelID = item.channelID,
|
|
||||||
let channel = viewModel.findChannel(id: channelID),
|
|
||||||
let mediaSource = channel.mediaSources?.first else { return }
|
|
||||||
|
|
||||||
router.route(to: \.videoPlayer, LiveVideoPlayerManager(item: channel, mediaSource: mediaSource))
|
|
||||||
} label: {
|
|
||||||
LandscapeItemElement(item: item)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
|
||||||
}
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
}
|
|
||||||
}.frame(height: 350)
|
|
||||||
}
|
|
||||||
if viewModel.newsItems.isNotEmpty {
|
|
||||||
let items = viewModel.newsItems
|
|
||||||
Text("News")
|
|
||||||
.font(.headline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.padding(.leading, 90)
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack {
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
ForEach(items, id: \.id) { item in
|
|
||||||
Button {
|
|
||||||
guard let channelID = item.channelID,
|
|
||||||
let channel = viewModel.findChannel(id: channelID),
|
|
||||||
let mediaSource = channel.mediaSources?.first else { return }
|
|
||||||
|
|
||||||
router.route(to: \.videoPlayer, LiveVideoPlayerManager(item: channel, mediaSource: mediaSource))
|
|
||||||
} label: {
|
|
||||||
LandscapeItemElement(item: item)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
|
||||||
}
|
|
||||||
Spacer().frame(width: 45)
|
|
||||||
}
|
|
||||||
}.frame(height: 350)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
|
||||||
.edgesIgnoringSafeArea(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,8 +14,6 @@ import SwiftUI
|
||||||
|
|
||||||
struct MediaView: View {
|
struct MediaView: View {
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var mainRouter: MainCoordinator.Router
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var router: MediaCoordinator.Router
|
private var router: MediaCoordinator.Router
|
||||||
|
|
||||||
|
@ -36,7 +34,8 @@ struct MediaView: View {
|
||||||
filters: .default
|
filters: .default
|
||||||
)
|
)
|
||||||
router.route(to: \.library, viewModel)
|
router.route(to: \.library, viewModel)
|
||||||
case .downloads: ()
|
case .downloads:
|
||||||
|
assertionFailure("Downloads unavailable on tvOS")
|
||||||
case .favorites:
|
case .favorites:
|
||||||
let viewModel = ItemLibraryViewModel(
|
let viewModel = ItemLibraryViewModel(
|
||||||
title: L10n.favorites,
|
title: L10n.favorites,
|
||||||
|
@ -44,7 +43,7 @@ struct MediaView: View {
|
||||||
)
|
)
|
||||||
router.route(to: \.library, viewModel)
|
router.route(to: \.library, viewModel)
|
||||||
case .liveTV:
|
case .liveTV:
|
||||||
mainRouter.root(\.liveTV)
|
router.route(to: \.liveTV)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ProgramsView {
|
||||||
|
|
||||||
|
struct ProgramButtonContent: View {
|
||||||
|
|
||||||
|
let program: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
Text(program.channelName ?? .emptyDash)
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.backport
|
||||||
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
|
||||||
|
Text(program.displayTitle)
|
||||||
|
.font(.footnote.weight(.regular))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.backport
|
||||||
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
if let startDate = program.startDate {
|
||||||
|
Text(startDate, style: .time)
|
||||||
|
} else {
|
||||||
|
Text(String.emptyDash)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("-")
|
||||||
|
|
||||||
|
if let endDate = program.endDate {
|
||||||
|
Text(endDate, style: .time)
|
||||||
|
} else {
|
||||||
|
Text(String.emptyDash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ProgramsView {
|
||||||
|
|
||||||
|
struct ProgramProgressOverlay: View {
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var programProgress: Double = 0.0
|
||||||
|
|
||||||
|
let program: BaseItemDto
|
||||||
|
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WrappedView {
|
||||||
|
if let startDate = program.startDate, startDate < Date.now {
|
||||||
|
LandscapePosterProgressBar(
|
||||||
|
progress: program.programProgress ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(timer) { newValue in
|
||||||
|
if let startDate = program.startDate, startDate < newValue, let duration = program.programDuration {
|
||||||
|
programProgress = newValue.timeIntervalSince(startDate) / duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: background refresh for programs with timer?
|
||||||
|
|
||||||
|
// Note: there are some unsafe first element accesses, but `ChannelProgram` data should always have a single program
|
||||||
|
|
||||||
|
struct ProgramsView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: VideoPlayerWrapperCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var programsViewModel = ProgramsViewModel()
|
||||||
|
|
||||||
|
private var contentView: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
if programsViewModel.recommended.isNotEmpty {
|
||||||
|
programsSection(title: L10n.onNow, keyPath: \.recommended)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.series.isNotEmpty {
|
||||||
|
programsSection(title: L10n.series, keyPath: \.series)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.movies.isNotEmpty {
|
||||||
|
programsSection(title: L10n.movies, keyPath: \.movies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.kids.isNotEmpty {
|
||||||
|
programsSection(title: L10n.kids, keyPath: \.kids)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.sports.isNotEmpty {
|
||||||
|
programsSection(title: L10n.sports, keyPath: \.sports)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.news.isNotEmpty {
|
||||||
|
programsSection(title: L10n.news, keyPath: \.news)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func programsSection(
|
||||||
|
title: String,
|
||||||
|
keyPath: KeyPath<ProgramsViewModel, [ChannelProgram]>
|
||||||
|
) -> some View {
|
||||||
|
PosterHStack(
|
||||||
|
title: title,
|
||||||
|
type: .landscape,
|
||||||
|
items: programsViewModel[keyPath: keyPath]
|
||||||
|
)
|
||||||
|
.content {
|
||||||
|
ProgramButtonContent(program: $0.programs[0])
|
||||||
|
}
|
||||||
|
.imageOverlay {
|
||||||
|
ProgramProgressOverlay(program: $0.programs[0])
|
||||||
|
}
|
||||||
|
.onSelect { channelProgram in
|
||||||
|
guard let mediaSource = channelProgram.channel.mediaSources?.first else { return }
|
||||||
|
router.route(
|
||||||
|
to: \.liveVideoPlayer,
|
||||||
|
LiveVideoPlayerManager(item: channelProgram.channel, mediaSource: mediaSource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WrappedView {
|
||||||
|
switch programsViewModel.state {
|
||||||
|
case .content:
|
||||||
|
if programsViewModel.hasNoResults {
|
||||||
|
L10n.noResults.text
|
||||||
|
} else {
|
||||||
|
contentView
|
||||||
|
}
|
||||||
|
case let .error(error):
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
case .initial, .refreshing:
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(edges: [.bottom, .horizontal])
|
||||||
|
.onFirstAppear {
|
||||||
|
if programsViewModel.state == .initial {
|
||||||
|
programsViewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,9 +44,6 @@ struct LiveNativeVideoPlayer: View {
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onDisappear {
|
|
||||||
NotificationCenter.default.post(name: .livePlayerDismissed, object: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,14 +53,14 @@ extension LiveVideoPlayer.Overlay {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .VideoPlayerTitleAlignmentGuide, spacing: 10) {
|
VStack(alignment: .VideoPlayerTitleAlignmentGuide, spacing: 10) {
|
||||||
|
|
||||||
if let subtitle = videoPlayerManager.program?.currentProgram?.programDisplayText(timeFormatter: DateFormatter()) {
|
// if let subtitle = videoPlayerManager.program?.currentProgram?.programDisplayText(timeFormatter: DateFormatter()) {
|
||||||
Text(subtitle.title)
|
// Text(subtitle.title)
|
||||||
.font(.subheadline)
|
// .font(.subheadline)
|
||||||
.foregroundColor(.white)
|
// .foregroundColor(.white)
|
||||||
.alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
|
// .alignmentGuide(.VideoPlayerTitleAlignmentGuide) { dimensions in
|
||||||
dimensions[.leading]
|
// dimensions[.leading]
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
|
|
|
@ -80,9 +80,6 @@ struct LiveVideoPlayer: View {
|
||||||
.scrubbedSeconds = Int(CGFloat(videoPlayerManager.currentViewModel.item.runTimeSeconds) * newValue)
|
.scrubbedSeconds = Int(CGFloat(videoPlayerManager.currentViewModel.item.runTimeSeconds) * newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
|
||||||
NotificationCenter.default.post(name: .livePlayerDismissed, object: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -149,15 +149,9 @@
|
||||||
BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */; };
|
BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */; };
|
||||||
BD0BA22E2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */; };
|
BD0BA22E2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */; };
|
||||||
BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */; };
|
BD0BA22F2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */; };
|
||||||
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 */; };
|
|
||||||
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
|
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
|
||||||
C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */; };
|
C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */; };
|
||||||
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */; };
|
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */; };
|
||||||
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */; };
|
|
||||||
C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
|
|
||||||
C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
|
||||||
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */; };
|
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */; };
|
||||||
C45C36552A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */; };
|
C45C36552A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */; };
|
||||||
C46008742A97DFF2002B1C7A /* LiveLoadingOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46008732A97DFF2002B1C7A /* LiveLoadingOverlay.swift */; };
|
C46008742A97DFF2002B1C7A /* LiveLoadingOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46008732A97DFF2002B1C7A /* LiveLoadingOverlay.swift */; };
|
||||||
|
@ -174,27 +168,33 @@
|
||||||
C46DD8EA2A8FB45C0046A504 /* LiveOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8E92A8FB45C0046A504 /* LiveOverlay.swift */; };
|
C46DD8EA2A8FB45C0046A504 /* LiveOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8E92A8FB45C0046A504 /* LiveOverlay.swift */; };
|
||||||
C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */; };
|
C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */; };
|
||||||
C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */; };
|
C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */; };
|
||||||
C49AE1182BC191BB004562B2 /* LiveTVChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49AE1172BC191BB004562B2 /* LiveTVChannelProgram.swift */; };
|
|
||||||
C49AE1192BC191BB004562B2 /* LiveTVChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49AE1172BC191BB004562B2 /* LiveTVChannelProgram.swift */; };
|
|
||||||
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; };
|
|
||||||
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */; };
|
|
||||||
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; };
|
|
||||||
C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
|
|
||||||
C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */; };
|
|
||||||
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; };
|
|
||||||
C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; };
|
|
||||||
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; };
|
|
||||||
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; };
|
|
||||||
C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; };
|
|
||||||
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
|
||||||
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
|
||||||
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; };
|
|
||||||
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* MediaView.swift */; };
|
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* MediaView.swift */; };
|
||||||
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */; };
|
|
||||||
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfo.swift */; };
|
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfo.swift */; };
|
||||||
E1002B652793CEE800E47059 /* ChapterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfo.swift */; };
|
E1002B652793CEE800E47059 /* ChapterInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfo.swift */; };
|
||||||
E1002B682793CFBA00E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B672793CFBA00E47059 /* Algorithms */; };
|
E1002B682793CFBA00E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B672793CFBA00E47059 /* Algorithms */; };
|
||||||
E1002B6B2793E36600E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B6A2793E36600E47059 /* Algorithms */; };
|
E1002B6B2793E36600E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B6A2793E36600E47059 /* Algorithms */; };
|
||||||
|
E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231292BCF8A08009D71FC /* iOSLiveTVCoordinator.swift */; };
|
||||||
|
E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E102312A2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift */; };
|
||||||
|
E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */; };
|
||||||
|
E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.swift */; };
|
||||||
|
E102313D2BCF8A3C009D71FC /* ProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231332BCF8A3C009D71FC /* ProgramsView.swift */; };
|
||||||
|
E102313F2BCF8A3C009D71FC /* WideChannelGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231352BCF8A3C009D71FC /* WideChannelGridItem.swift */; };
|
||||||
|
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */; };
|
||||||
|
E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231432BCF8A51009D71FC /* ChannelProgram.swift */; };
|
||||||
|
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231432BCF8A51009D71FC /* ChannelProgram.swift */; };
|
||||||
|
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */; };
|
||||||
|
E10231492BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */; };
|
||||||
|
E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */; };
|
||||||
|
E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */; };
|
||||||
|
E102314D2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */; };
|
||||||
|
E102314E2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */; };
|
||||||
|
E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E102314F2BCF8AF8009D71FC /* WideChannelGridItem.swift */; };
|
||||||
|
E10231592BCF8AF8009D71FC /* ChannelLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231512BCF8AF8009D71FC /* ChannelLibraryView.swift */; };
|
||||||
|
E102315A2BCF8AF8009D71FC /* ProgramButtonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231532BCF8AF8009D71FC /* ProgramButtonContent.swift */; };
|
||||||
|
E102315B2BCF8AF8009D71FC /* ProgramProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231542BCF8AF8009D71FC /* ProgramProgressOverlay.swift */; };
|
||||||
|
E102315C2BCF8AF8009D71FC /* ProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10231562BCF8AF8009D71FC /* ProgramsView.swift */; };
|
||||||
|
E102315F2BCF8B75009D71FC /* VideoPlayerWrapperCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E102315E2BCF8B75009D71FC /* VideoPlayerWrapperCoordinator.swift */; };
|
||||||
|
E10231602BCF8B7E009D71FC /* VideoPlayerWrapperCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E102315E2BCF8B75009D71FC /* VideoPlayerWrapperCoordinator.swift */; };
|
||||||
E103DF902BCF2F1C000229B2 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103DF8F2BCF2F1C000229B2 /* MediaItem.swift */; };
|
E103DF902BCF2F1C000229B2 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103DF8F2BCF2F1C000229B2 /* MediaItem.swift */; };
|
||||||
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103DF942BCF31CD000229B2 /* MediaItem.swift */; };
|
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103DF942BCF31CD000229B2 /* MediaItem.swift */; };
|
||||||
E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */; };
|
E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */; };
|
||||||
|
@ -563,7 +563,6 @@
|
||||||
E18E021E2887492B0022598C /* RowDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* RowDivider.swift */; };
|
E18E021E2887492B0022598C /* RowDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* RowDivider.swift */; };
|
||||||
E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */; };
|
E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */; };
|
||||||
E18E02232887492B0022598C /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; };
|
E18E02232887492B0022598C /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; };
|
||||||
E18E02252887492B0022598C /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; };
|
|
||||||
E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1921B7328E61914003A5238 /* SpecialFeatureHStack.swift */; };
|
E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1921B7328E61914003A5238 /* SpecialFeatureHStack.swift */; };
|
||||||
E1921B7628E63306003A5238 /* GestureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1921B7528E63306003A5238 /* GestureView.swift */; };
|
E1921B7628E63306003A5238 /* GestureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1921B7528E63306003A5238 /* GestureView.swift */; };
|
||||||
E192608028D28AAD002314B4 /* UserProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E192607F28D28AAD002314B4 /* UserProfileButton.swift */; };
|
E192608028D28AAD002314B4 /* UserProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E192607F28D28AAD002314B4 /* UserProfileButton.swift */; };
|
||||||
|
@ -641,7 +640,6 @@
|
||||||
E1B5F7AD29577BDD004B26CF /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E1B5F7AC29577BDD004B26CF /* OrderedCollections */; };
|
E1B5F7AD29577BDD004B26CF /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E1B5F7AC29577BDD004B26CF /* OrderedCollections */; };
|
||||||
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */; };
|
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */; };
|
||||||
E1B90C8A2BC475E7007027C8 /* ScalingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */; };
|
E1B90C8A2BC475E7007027C8 /* ScalingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */; };
|
||||||
E1BA6FC529D25DBD007D98DC /* LandscapeItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BA6FC429D25DBD007D98DC /* LandscapeItemElement.swift */; };
|
|
||||||
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */; };
|
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */; };
|
||||||
E1BDF2E62951475300CC0294 /* VideoPlayerActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */; };
|
E1BDF2E62951475300CC0294 /* VideoPlayerActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */; };
|
||||||
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E82951490400CC0294 /* ActionButtonSelectorView.swift */; };
|
E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E82951490400CC0294 /* ActionButtonSelectorView.swift */; };
|
||||||
|
@ -834,7 +832,6 @@
|
||||||
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||||
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
|
4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = "<group>"; };
|
||||||
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||||
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = "<group>"; };
|
|
||||||
531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
|
531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
|
||||||
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = "<group>"; };
|
5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -953,12 +950,9 @@
|
||||||
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>"; };
|
||||||
BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineVideoPlayerManager.swift; sourceTree = "<group>"; };
|
BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineVideoPlayerManager.swift; sourceTree = "<group>"; };
|
||||||
BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadVideoPlayerManager.swift; sourceTree = "<group>"; };
|
BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadVideoPlayerManager.swift; 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>"; };
|
|
||||||
C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = "<group>"; };
|
C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = "<group>"; };
|
||||||
C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = "<group>"; };
|
C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = "<group>"; };
|
||||||
C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLargePlaybackButtons.swift; sourceTree = "<group>"; };
|
C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLargePlaybackButtons.swift; sourceTree = "<group>"; };
|
||||||
C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVCoordinator.swift; sourceTree = "<group>"; };
|
|
||||||
C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveVideoPlayerManager.swift; sourceTree = "<group>"; };
|
C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveVideoPlayerManager.swift; sourceTree = "<group>"; };
|
||||||
C46008732A97DFF2002B1C7A /* LiveLoadingOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLoadingOverlay.swift; sourceTree = "<group>"; };
|
C46008732A97DFF2002B1C7A /* LiveLoadingOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLoadingOverlay.swift; sourceTree = "<group>"; };
|
||||||
C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
@ -973,21 +967,25 @@
|
||||||
C46DD8E92A8FB45C0046A504 /* LiveOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveOverlay.swift; sourceTree = "<group>"; };
|
C46DD8E92A8FB45C0046A504 /* LiveOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveOverlay.swift; sourceTree = "<group>"; };
|
||||||
C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMainOverlay.swift; sourceTree = "<group>"; };
|
C46DD8EB2A8FB49A0046A504 /* LiveMainOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMainOverlay.swift; sourceTree = "<group>"; };
|
||||||
C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveBottomBarView.swift; sourceTree = "<group>"; };
|
C46DD8EE2A8FB56E0046A504 /* LiveBottomBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveBottomBarView.swift; sourceTree = "<group>"; };
|
||||||
C49AE1172BC191BB004562B2 /* LiveTVChannelProgram.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelProgram.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>"; };
|
|
||||||
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsCoordinator.swift; sourceTree = "<group>"; };
|
|
||||||
C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = "<group>"; };
|
|
||||||
C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVTabCoordinator.swift; sourceTree = "<group>"; };
|
|
||||||
C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsCoordinator.swift; sourceTree = "<group>"; };
|
|
||||||
C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = "<group>"; };
|
|
||||||
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = "<group>"; };
|
|
||||||
C4E508172703E8190045C9AB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
|
C4E508172703E8190045C9AB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.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>"; };
|
|
||||||
E1002B632793CEE700E47059 /* ChapterInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfo.swift; sourceTree = "<group>"; };
|
E1002B632793CEE700E47059 /* ChapterInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfo.swift; sourceTree = "<group>"; };
|
||||||
|
E10231292BCF8A08009D71FC /* iOSLiveTVCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iOSLiveTVCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
E102312A2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramButtonContent.swift; sourceTree = "<group>"; };
|
||||||
|
E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramProgressOverlay.swift; sourceTree = "<group>"; };
|
||||||
|
E10231332BCF8A3C009D71FC /* ProgramsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramsView.swift; sourceTree = "<group>"; };
|
||||||
|
E10231352BCF8A3C009D71FC /* WideChannelGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WideChannelGridItem.swift; sourceTree = "<group>"; };
|
||||||
|
E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLibraryView.swift; sourceTree = "<group>"; };
|
||||||
|
E10231432BCF8A51009D71FC /* ChannelProgram.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelProgram.swift; sourceTree = "<group>"; };
|
||||||
|
E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLibraryViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlternateLayoutView.swift; sourceTree = "<group>"; };
|
||||||
|
E102314F2BCF8AF8009D71FC /* WideChannelGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WideChannelGridItem.swift; sourceTree = "<group>"; };
|
||||||
|
E10231512BCF8AF8009D71FC /* ChannelLibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelLibraryView.swift; sourceTree = "<group>"; };
|
||||||
|
E10231532BCF8AF8009D71FC /* ProgramButtonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramButtonContent.swift; sourceTree = "<group>"; };
|
||||||
|
E10231542BCF8AF8009D71FC /* ProgramProgressOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramProgressOverlay.swift; sourceTree = "<group>"; };
|
||||||
|
E10231562BCF8AF8009D71FC /* ProgramsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramsView.swift; sourceTree = "<group>"; };
|
||||||
|
E102315E2BCF8B75009D71FC /* VideoPlayerWrapperCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerWrapperCoordinator.swift; sourceTree = "<group>"; };
|
||||||
E103DF8F2BCF2F1C000229B2 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
|
E103DF8F2BCF2F1C000229B2 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
|
||||||
E103DF942BCF31CD000229B2 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
|
E103DF942BCF31CD000229B2 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
|
||||||
E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemImageContentView.swift; sourceTree = "<group>"; };
|
E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemImageContentView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1246,7 +1244,6 @@
|
||||||
E1B5784028F8AFCB00D42911 /* WrappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedView.swift; sourceTree = "<group>"; };
|
E1B5784028F8AFCB00D42911 /* WrappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedView.swift; sourceTree = "<group>"; };
|
||||||
E1B5861129E32EEF00E45D6E /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = "<group>"; };
|
E1B5861129E32EEF00E45D6E /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = "<group>"; };
|
||||||
E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetScrollView.swift; sourceTree = "<group>"; };
|
E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetScrollView.swift; sourceTree = "<group>"; };
|
||||||
E1BA6FC429D25DBD007D98DC /* LandscapeItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeItemElement.swift; sourceTree = "<group>"; };
|
|
||||||
E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerActionButton.swift; sourceTree = "<group>"; };
|
E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerActionButton.swift; sourceTree = "<group>"; };
|
||||||
E1BDF2E82951490400CC0294 /* ActionButtonSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonSelectorView.swift; sourceTree = "<group>"; };
|
E1BDF2E82951490400CC0294 /* ActionButtonSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonSelectorView.swift; sourceTree = "<group>"; };
|
||||||
E1BDF2EB2952290200CC0294 /* AspectFillActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AspectFillActionButton.swift; sourceTree = "<group>"; };
|
E1BDF2EB2952290200CC0294 /* AspectFillActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AspectFillActionButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1507,15 +1504,16 @@
|
||||||
532175392671BCED005491E6 /* ViewModels */ = {
|
532175392671BCED005491E6 /* ViewModels */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */,
|
||||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||||
E17AC96E2954EE4B003D2BC2 /* DownloadListViewModel.swift */,
|
E17AC96E2954EE4B003D2BC2 /* DownloadListViewModel.swift */,
|
||||||
E113133928BEB71D00930F75 /* FilterViewModel.swift */,
|
E113133928BEB71D00930F75 /* FilterViewModel.swift */,
|
||||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
||||||
E107BB9127880A4000354E07 /* ItemViewModel */,
|
E107BB9127880A4000354E07 /* ItemViewModel */,
|
||||||
E1EDA8D52B924CA500F9A57E /* LibraryViewModel */,
|
E1EDA8D52B924CA500F9A57E /* LibraryViewModel */,
|
||||||
C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */,
|
C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */,
|
||||||
C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */,
|
|
||||||
E1CAF65C2BA345830087D991 /* MediaViewModel */,
|
E1CAF65C2BA345830087D991 /* MediaViewModel */,
|
||||||
|
E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */,
|
||||||
6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */,
|
6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */,
|
||||||
62E632DB267D2E130063E547 /* SearchViewModel.swift */,
|
62E632DB267D2E130063E547 /* SearchViewModel.swift */,
|
||||||
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */,
|
E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */,
|
||||||
|
@ -1524,7 +1522,6 @@
|
||||||
E13DD3F82717E961009D4DAF /* UserListViewModel.swift */,
|
E13DD3F82717E961009D4DAF /* UserListViewModel.swift */,
|
||||||
E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */,
|
E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */,
|
||||||
BD0BA2292AD6501300306A8D /* VideoPlayerManager */,
|
BD0BA2292AD6501300306A8D /* VideoPlayerManager */,
|
||||||
C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */,
|
|
||||||
E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */,
|
E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */,
|
||||||
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1613,6 +1610,7 @@
|
||||||
children = (
|
children = (
|
||||||
E1D4BF802719D22800A11E64 /* AppAppearance.swift */,
|
E1D4BF802719D22800A11E64 /* AppAppearance.swift */,
|
||||||
E129429728F4785200796AC6 /* CaseIterablePicker.swift */,
|
E129429728F4785200796AC6 /* CaseIterablePicker.swift */,
|
||||||
|
E10231432BCF8A51009D71FC /* ChannelProgram.swift */,
|
||||||
E17FB55128C119D400311DFE /* Displayable.swift */,
|
E17FB55128C119D400311DFE /* Displayable.swift */,
|
||||||
E1579EA62B97DC1500A31CA1 /* Eventful.swift */,
|
E1579EA62B97DC1500A31CA1 /* Eventful.swift */,
|
||||||
E1092F4B29106F9F00163F57 /* GestureAction.swift */,
|
E1092F4B29106F9F00163F57 /* GestureAction.swift */,
|
||||||
|
@ -1642,7 +1640,6 @@
|
||||||
E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */,
|
E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */,
|
||||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
|
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
|
||||||
E15756352936856700976E1F /* VideoPlayerType.swift */,
|
E15756352936856700976E1F /* VideoPlayerType.swift */,
|
||||||
C49AE1172BC191BB004562B2 /* LiveTVChannelProgram.swift */,
|
|
||||||
);
|
);
|
||||||
path = Objects;
|
path = Objects;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1656,9 +1653,7 @@
|
||||||
E1C92618288756BD002A7A66 /* DotHStack.swift */,
|
E1C92618288756BD002A7A66 /* DotHStack.swift */,
|
||||||
E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */,
|
E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */,
|
||||||
E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */,
|
E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */,
|
||||||
E1BA6FC429D25DBD007D98DC /* LandscapeItemElement.swift */,
|
|
||||||
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */,
|
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */,
|
||||||
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
|
|
||||||
E10E842B29A589860064EA49 /* NonePosterButton.swift */,
|
E10E842B29A589860064EA49 /* NonePosterButton.swift */,
|
||||||
E111D8F928D0400900400001 /* PagingLibraryView.swift */,
|
E111D8F928D0400900400001 /* PagingLibraryView.swift */,
|
||||||
E1C92617288756BD002A7A66 /* PosterButton.swift */,
|
E1C92617288756BD002A7A66 /* PosterButton.swift */,
|
||||||
|
@ -1969,10 +1964,7 @@
|
||||||
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
||||||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
||||||
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
||||||
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
|
E102312B2BCF8A08009D71FC /* LiveTVCoordinator */,
|
||||||
C45942C427F67DA400C54FE7 /* LiveTVCoordinator.swift */,
|
|
||||||
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */,
|
|
||||||
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */,
|
|
||||||
C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */,
|
C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */,
|
||||||
E193D5412719404B00900D82 /* MainCoordinator */,
|
E193D5412719404B00900D82 /* MainCoordinator */,
|
||||||
62C29EA726D103D500C1D2E7 /* MediaCoordinator.swift */,
|
62C29EA726D103D500C1D2E7 /* MediaCoordinator.swift */,
|
||||||
|
@ -1986,6 +1978,7 @@
|
||||||
E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */,
|
E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */,
|
||||||
E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */,
|
E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */,
|
||||||
E1A1528C28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift */,
|
E1A1528C28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift */,
|
||||||
|
E102315E2BCF8B75009D71FC /* VideoPlayerWrapperCoordinator.swift */,
|
||||||
);
|
);
|
||||||
path = Coordinators;
|
path = Coordinators;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2056,6 +2049,93 @@
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E102312B2BCF8A08009D71FC /* LiveTVCoordinator */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231292BCF8A08009D71FC /* iOSLiveTVCoordinator.swift */,
|
||||||
|
E102312A2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift */,
|
||||||
|
);
|
||||||
|
path = LiveTVCoordinator;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231322BCF8A3C009D71FC /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231302BCF8A3C009D71FC /* ProgramButtonContent.swift */,
|
||||||
|
E10231312BCF8A3C009D71FC /* ProgramProgressOverlay.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231342BCF8A3C009D71FC /* ProgramsView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231322BCF8A3C009D71FC /* Components */,
|
||||||
|
E10231332BCF8A3C009D71FC /* ProgramsView.swift */,
|
||||||
|
);
|
||||||
|
path = ProgramsView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231362BCF8A3C009D71FC /* Component */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231352BCF8A3C009D71FC /* WideChannelGridItem.swift */,
|
||||||
|
);
|
||||||
|
path = Component;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231382BCF8A3C009D71FC /* ChannelLibraryView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231362BCF8A3C009D71FC /* Component */,
|
||||||
|
E10231372BCF8A3C009D71FC /* ChannelLibraryView.swift */,
|
||||||
|
);
|
||||||
|
path = ChannelLibraryView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231502BCF8AF8009D71FC /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E102314F2BCF8AF8009D71FC /* WideChannelGridItem.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231522BCF8AF8009D71FC /* ChannelLibraryView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231502BCF8AF8009D71FC /* Components */,
|
||||||
|
E10231512BCF8AF8009D71FC /* ChannelLibraryView.swift */,
|
||||||
|
);
|
||||||
|
path = ChannelLibraryView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231552BCF8AF8009D71FC /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231532BCF8AF8009D71FC /* ProgramButtonContent.swift */,
|
||||||
|
E10231542BCF8AF8009D71FC /* ProgramProgressOverlay.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E10231572BCF8AF8009D71FC /* ProgramsView */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E10231552BCF8AF8009D71FC /* Components */,
|
||||||
|
E10231562BCF8AF8009D71FC /* ProgramsView.swift */,
|
||||||
|
);
|
||||||
|
path = ProgramsView;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E102315D2BCF8B36009D71FC /* Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E103DF942BCF31CD000229B2 /* MediaItem.swift */,
|
||||||
|
);
|
||||||
|
path = Components;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E103DF912BCF2F1F000229B2 /* Components */ = {
|
E103DF912BCF2F1F000229B2 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2076,8 +2156,8 @@
|
||||||
E103DF932BCF31C5000229B2 /* MediaView */ = {
|
E103DF932BCF31C5000229B2 /* MediaView */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E102315D2BCF8B36009D71FC /* Components */,
|
||||||
C4E508172703E8190045C9AB /* MediaView.swift */,
|
C4E508172703E8190045C9AB /* MediaView.swift */,
|
||||||
E103DF942BCF31CD000229B2 /* MediaItem.swift */,
|
|
||||||
);
|
);
|
||||||
path = MediaView;
|
path = MediaView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2228,16 +2308,15 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */,
|
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */,
|
||||||
|
E10231522BCF8AF8009D71FC /* ChannelLibraryView */,
|
||||||
53ABFDEA2679753200886593 /* ConnectToServerView.swift */,
|
53ABFDEA2679753200886593 /* ConnectToServerView.swift */,
|
||||||
E154967B296CBB1A00C4EF88 /* FontPickerView.swift */,
|
E154967B296CBB1A00C4EF88 /* FontPickerView.swift */,
|
||||||
E1A42E4D28CBD3B200A14DCB /* HomeView */,
|
E1A42E4D28CBD3B200A14DCB /* HomeView */,
|
||||||
E12376B22A33DFAC001F5B44 /* ItemOverviewView.swift */,
|
E12376B22A33DFAC001F5B44 /* ItemOverviewView.swift */,
|
||||||
E193D54E271942C000900D82 /* ItemView */,
|
E193D54E271942C000900D82 /* ItemView */,
|
||||||
C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */,
|
|
||||||
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */,
|
|
||||||
C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */,
|
|
||||||
E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */,
|
E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */,
|
||||||
E103DF932BCF31C5000229B2 /* MediaView */,
|
E103DF932BCF31C5000229B2 /* MediaView */,
|
||||||
|
E10231572BCF8AF8009D71FC /* ProgramsView */,
|
||||||
E1E1643928BAC2EF00323B0A /* SearchView.swift */,
|
E1E1643928BAC2EF00323B0A /* SearchView.swift */,
|
||||||
E193D54F2719430400900D82 /* ServerDetailView.swift */,
|
E193D54F2719430400900D82 /* ServerDetailView.swift */,
|
||||||
E193D54A271941D300900D82 /* ServerListView.swift */,
|
E193D54A271941D300900D82 /* ServerListView.swift */,
|
||||||
|
@ -2294,6 +2373,7 @@
|
||||||
E18E01F3288747580022598C /* AboutAppView.swift */,
|
E18E01F3288747580022598C /* AboutAppView.swift */,
|
||||||
E1401C9F2937DFF500E8B599 /* AppIconSelectorView.swift */,
|
E1401C9F2937DFF500E8B599 /* AppIconSelectorView.swift */,
|
||||||
E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */,
|
E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */,
|
||||||
|
E10231382BCF8A3C009D71FC /* ChannelLibraryView */,
|
||||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||||
E17AC96C2954E9CA003D2BC2 /* DownloadListView.swift */,
|
E17AC96C2954E9CA003D2BC2 /* DownloadListView.swift */,
|
||||||
E13332922953BA9400EE76AB /* DownloadTaskView */,
|
E13332922953BA9400EE76AB /* DownloadTaskView */,
|
||||||
|
@ -2302,15 +2382,11 @@
|
||||||
E168BD07289A4162001A6922 /* HomeView */,
|
E168BD07289A4162001A6922 /* HomeView */,
|
||||||
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
|
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
|
||||||
E14F7D0A26DB3714007C3AE6 /* ItemView */,
|
E14F7D0A26DB3714007C3AE6 /* ItemView */,
|
||||||
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */,
|
|
||||||
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */,
|
|
||||||
C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */,
|
|
||||||
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
|
||||||
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
|
||||||
E170D104294D21FA0017224C /* MediaSourceInfoView.swift */,
|
E170D104294D21FA0017224C /* MediaSourceInfoView.swift */,
|
||||||
E19F6C5C28F5189300C5197E /* MediaStreamInfoView.swift */,
|
E19F6C5C28F5189300C5197E /* MediaStreamInfoView.swift */,
|
||||||
E103DF922BCF2F23000229B2 /* MediaView */,
|
E103DF922BCF2F23000229B2 /* MediaView */,
|
||||||
E1EDA8D62B92C9D700F9A57E /* PagingLibraryView */,
|
E1EDA8D62B92C9D700F9A57E /* PagingLibraryView */,
|
||||||
|
E10231342BCF8A3C009D71FC /* ProgramsView */,
|
||||||
E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */,
|
E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */,
|
||||||
53EE24E5265060780068F029 /* SearchView.swift */,
|
53EE24E5265060780068F029 /* SearchView.swift */,
|
||||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
||||||
|
@ -2759,12 +2835,12 @@
|
||||||
E1AD105326D96F5A003E4A08 /* Components */ = {
|
E1AD105326D96F5A003E4A08 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E102314C2BCF8A7E009D71FC /* AlternateLayoutView.swift */,
|
||||||
E104DC952B9E7E29008F506D /* AssertionFailureView.swift */,
|
E104DC952B9E7E29008F506D /* AssertionFailureView.swift */,
|
||||||
E18E0203288749200022598C /* BlurView.swift */,
|
E18E0203288749200022598C /* BlurView.swift */,
|
||||||
E1153DCB2BBB633B00424D36 /* FastSVGView.swift */,
|
E1153DCB2BBB633B00424D36 /* FastSVGView.swift */,
|
||||||
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
||||||
E1D37F472B9C648E00343D2B /* MaxHeightText.swift */,
|
E1D37F472B9C648E00343D2B /* MaxHeightText.swift */,
|
||||||
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
|
|
||||||
E1DC983F296DEBA500982F06 /* PosterIndicators */,
|
E1DC983F296DEBA500982F06 /* PosterIndicators */,
|
||||||
E1FE69A628C29B720021BC93 /* ProgressBar.swift */,
|
E1FE69A628C29B720021BC93 /* ProgressBar.swift */,
|
||||||
E187A60129AB28F0008387E6 /* RotateContentView.swift */,
|
E187A60129AB28F0008387E6 /* RotateContentView.swift */,
|
||||||
|
@ -3402,7 +3478,6 @@
|
||||||
E15D4F0B2B1BD88900442DB8 /* Edge.swift in Sources */,
|
E15D4F0B2B1BD88900442DB8 /* Edge.swift in Sources */,
|
||||||
E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */,
|
E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */,
|
||||||
E1CCC3D228C858A50020ED54 /* UserProfileButton.swift in Sources */,
|
E1CCC3D228C858A50020ED54 /* UserProfileButton.swift in Sources */,
|
||||||
C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */,
|
|
||||||
E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
|
E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
|
||||||
E1B490452967E26300D3EDCE /* PersistentLogHandler.swift in Sources */,
|
E1B490452967E26300D3EDCE /* PersistentLogHandler.swift in Sources */,
|
||||||
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
|
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
|
||||||
|
@ -3431,6 +3506,7 @@
|
||||||
E187A60529AD2E25008387E6 /* StepperView.swift in Sources */,
|
E187A60529AD2E25008387E6 /* StepperView.swift in Sources */,
|
||||||
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */,
|
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */,
|
||||||
E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
|
E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
|
||||||
|
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
|
||||||
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */,
|
||||||
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
|
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
|
||||||
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
|
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
|
||||||
|
@ -3439,13 +3515,12 @@
|
||||||
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
|
||||||
E1C926162887565C002A7A66 /* SeriesItemView.swift in Sources */,
|
E1C926162887565C002A7A66 /* SeriesItemView.swift in Sources */,
|
||||||
E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */,
|
E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */,
|
||||||
|
E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
|
||||||
E1DD55382B6EE533007501C0 /* Task.swift in Sources */,
|
E1DD55382B6EE533007501C0 /* Task.swift in Sources */,
|
||||||
E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
|
E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
|
||||||
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
|
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
|
||||||
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
|
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
|
||||||
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */,
|
E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */,
|
||||||
C49AE1192BC191BB004562B2 /* LiveTVChannelProgram.swift in Sources */,
|
|
||||||
C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
|
||||||
E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */,
|
E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */,
|
||||||
E1C9260C2887565C002A7A66 /* MovieItemContentView.swift in Sources */,
|
E1C9260C2887565C002A7A66 /* MovieItemContentView.swift in Sources */,
|
||||||
E1C9260B2887565C002A7A66 /* MovieItemView.swift in Sources */,
|
E1C9260B2887565C002A7A66 /* MovieItemView.swift in Sources */,
|
||||||
|
@ -3454,13 +3529,14 @@
|
||||||
E1E9EFEB28C7EA2C00CC1F8B /* UserDto.swift in Sources */,
|
E1E9EFEB28C7EA2C00CC1F8B /* UserDto.swift in Sources */,
|
||||||
E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */,
|
E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */,
|
||||||
E1575EA6293E7D40001665B1 /* VideoPlayer.swift in Sources */,
|
E1575EA6293E7D40001665B1 /* VideoPlayer.swift in Sources */,
|
||||||
E1BA6FC529D25DBD007D98DC /* LandscapeItemElement.swift in Sources */,
|
|
||||||
E185920628CDAA6400326F80 /* CastAndCrewHStack.swift in Sources */,
|
E185920628CDAA6400326F80 /* CastAndCrewHStack.swift in Sources */,
|
||||||
E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */,
|
E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */,
|
||||||
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
|
||||||
E122A9142788EAAD0060FA63 /* MediaStream.swift in Sources */,
|
E122A9142788EAAD0060FA63 /* MediaStream.swift in Sources */,
|
||||||
|
E102314E2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */,
|
||||||
E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */,
|
E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */,
|
||||||
E1575E85293E7A00001665B1 /* DarkAppIcon.swift in Sources */,
|
E1575E85293E7A00001665B1 /* DarkAppIcon.swift in Sources */,
|
||||||
|
E102315A2BCF8AF8009D71FC /* ProgramButtonContent.swift in Sources */,
|
||||||
C45C36552A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
C45C36552A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
||||||
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */,
|
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */,
|
||||||
E1575E95293E7B1E001665B1 /* Font.swift in Sources */,
|
E1575E95293E7B1E001665B1 /* Font.swift in Sources */,
|
||||||
|
@ -3489,7 +3565,6 @@
|
||||||
E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */,
|
E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */,
|
||||||
E1722DB229491C3900CC0239 /* ImageBlurHashes.swift in Sources */,
|
E1722DB229491C3900CC0239 /* ImageBlurHashes.swift in Sources */,
|
||||||
E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */,
|
E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */,
|
||||||
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */,
|
|
||||||
E12CC1BC28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
E12CC1BC28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
||||||
E1575E9E293E7B1E001665B1 /* Equatable.swift in Sources */,
|
E1575E9E293E7B1E001665B1 /* Equatable.swift in Sources */,
|
||||||
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */,
|
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */,
|
||||||
|
@ -3505,7 +3580,6 @@
|
||||||
E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */,
|
E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */,
|
||||||
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */,
|
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */,
|
||||||
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
|
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
|
||||||
E18E02252887492B0022598C /* PlainNavigationLinkButton.swift in Sources */,
|
|
||||||
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||||
62E632E1267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */,
|
62E632E1267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */,
|
||||||
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||||
|
@ -3516,7 +3590,6 @@
|
||||||
E1E2F8432B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */,
|
E1E2F8432B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */,
|
||||||
E111D8F628D03B7500400001 /* PagingLibraryViewModel.swift in Sources */,
|
E111D8F628D03B7500400001 /* PagingLibraryViewModel.swift in Sources */,
|
||||||
E1575E87293E7A00001665B1 /* InvertedDarkAppIcon.swift in Sources */,
|
E1575E87293E7A00001665B1 /* InvertedDarkAppIcon.swift in Sources */,
|
||||||
C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */,
|
|
||||||
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||||
E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
|
E1549667296CA2EF00C4EF88 /* SwiftfinNotifications.swift in Sources */,
|
||||||
|
@ -3537,13 +3610,11 @@
|
||||||
E185920828CDAAA200326F80 /* SimilarItemsHStack.swift in Sources */,
|
E185920828CDAAA200326F80 /* SimilarItemsHStack.swift in Sources */,
|
||||||
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */,
|
E10E842C29A589860064EA49 /* NonePosterButton.swift in Sources */,
|
||||||
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */,
|
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */,
|
||||||
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */,
|
|
||||||
E1DC9842296DEBD800982F06 /* WatchedIndicator.swift in Sources */,
|
E1DC9842296DEBD800982F06 /* WatchedIndicator.swift in Sources */,
|
||||||
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */,
|
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */,
|
||||||
E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */,
|
E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */,
|
||||||
E10E842A29A587110064EA49 /* LoadingView.swift in Sources */,
|
E10E842A29A587110064EA49 /* LoadingView.swift in Sources */,
|
||||||
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
|
||||||
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
|
|
||||||
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */,
|
E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */,
|
||||||
E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
||||||
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */,
|
E43918672AD5C8310045A18C /* ScenePhaseChangeModifier.swift in Sources */,
|
||||||
|
@ -3569,7 +3640,6 @@
|
||||||
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
|
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
|
||||||
E14EDEC62B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */,
|
E14EDEC62B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */,
|
||||||
E14EDEC92B8FB65F000F00A4 /* ItemFilterType.swift in Sources */,
|
E14EDEC92B8FB65F000F00A4 /* ItemFilterType.swift in Sources */,
|
||||||
C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
|
||||||
E1D37F4C2B9CEA5C00343D2B /* ImageSource.swift in Sources */,
|
E1D37F4C2B9CEA5C00343D2B /* ImageSource.swift in Sources */,
|
||||||
E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */,
|
||||||
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */,
|
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */,
|
||||||
|
@ -3580,7 +3650,9 @@
|
||||||
E1575E72293E77B5001665B1 /* Utilities.swift in Sources */,
|
E1575E72293E77B5001665B1 /* Utilities.swift in Sources */,
|
||||||
E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */,
|
E1575E84293E7A00001665B1 /* PrimaryAppIcon.swift in Sources */,
|
||||||
E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */,
|
E1153DCD2BBB633B00424D36 /* FastSVGView.swift in Sources */,
|
||||||
|
E102315B2BCF8AF8009D71FC /* ProgramProgressOverlay.swift in Sources */,
|
||||||
E1E6C45129B104850064123F /* Button.swift in Sources */,
|
E1E6C45129B104850064123F /* Button.swift in Sources */,
|
||||||
|
E102315C2BCF8AF8009D71FC /* ProgramsView.swift in Sources */,
|
||||||
E1DC981A296DD1CD00982F06 /* CinematicBackgroundView.swift in Sources */,
|
E1DC981A296DD1CD00982F06 /* CinematicBackgroundView.swift in Sources */,
|
||||||
E1A1528B28FD22F600600579 /* TextPairView.swift in Sources */,
|
E1A1528B28FD22F600600579 /* TextPairView.swift in Sources */,
|
||||||
E11042762B8013DF00821020 /* Stateful.swift in Sources */,
|
E11042762B8013DF00821020 /* Stateful.swift in Sources */,
|
||||||
|
@ -3638,10 +3710,11 @@
|
||||||
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
||||||
E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */,
|
E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */,
|
||||||
E1D37F532B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift in Sources */,
|
E1D37F532B9CEF1E00343D2B /* DeviceProfile+SharedCodecProfiles.swift in Sources */,
|
||||||
C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */,
|
|
||||||
E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */,
|
E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */,
|
||||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||||
|
E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */,
|
||||||
E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */,
|
E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */,
|
||||||
|
E102315F2BCF8B75009D71FC /* VideoPlayerWrapperCoordinator.swift in Sources */,
|
||||||
E1C9260D2887565C002A7A66 /* CinematicScrollView.swift in Sources */,
|
E1C9260D2887565C002A7A66 /* CinematicScrollView.swift in Sources */,
|
||||||
E1575E90293E7B1E001665B1 /* EdgeInsets.swift in Sources */,
|
E1575E90293E7B1E001665B1 /* EdgeInsets.swift in Sources */,
|
||||||
E1E6C43D29AECC310064123F /* BarActionButtons.swift in Sources */,
|
E1E6C43D29AECC310064123F /* BarActionButtons.swift in Sources */,
|
||||||
|
@ -3665,6 +3738,7 @@
|
||||||
E1E6C44E29AEE9DC0064123F /* SmallMenuOverlay.swift in Sources */,
|
E1E6C44E29AEE9DC0064123F /* SmallMenuOverlay.swift in Sources */,
|
||||||
E154966B296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
E154966B296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
||||||
535870632669D21600D05A09 /* SwiftfinApp.swift in Sources */,
|
535870632669D21600D05A09 /* SwiftfinApp.swift in Sources */,
|
||||||
|
E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */,
|
||||||
E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */,
|
E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */,
|
||||||
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */,
|
||||||
E1575E9A293E7B1E001665B1 /* Array.swift in Sources */,
|
E1575E9A293E7B1E001665B1 /* Array.swift in Sources */,
|
||||||
|
@ -3676,19 +3750,20 @@
|
||||||
E193D547271941C500900D82 /* UserListView.swift in Sources */,
|
E193D547271941C500900D82 /* UserListView.swift in Sources */,
|
||||||
E1DE2B4D2B9838B200F6715F /* PaletteOverlayRenderingModifier.swift in Sources */,
|
E1DE2B4D2B9838B200F6715F /* PaletteOverlayRenderingModifier.swift in Sources */,
|
||||||
E1BDF2E62951475300CC0294 /* VideoPlayerActionButton.swift in Sources */,
|
E1BDF2E62951475300CC0294 /* VideoPlayerActionButton.swift in Sources */,
|
||||||
|
E10231592BCF8AF8009D71FC /* ChannelLibraryView.swift in Sources */,
|
||||||
E1E6C44929AECEE70064123F /* AutoPlayActionButton.swift in Sources */,
|
E1E6C44929AECEE70064123F /* AutoPlayActionButton.swift in Sources */,
|
||||||
E1C926152887565C002A7A66 /* EpisodeCard.swift in Sources */,
|
E1C926152887565C002A7A66 /* EpisodeCard.swift in Sources */,
|
||||||
E1A1528E28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */,
|
E1A1528E28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */,
|
||||||
E18E021D2887492B0022598C /* AttributeStyleModifier.swift in Sources */,
|
E18E021D2887492B0022598C /* AttributeStyleModifier.swift in Sources */,
|
||||||
E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */,
|
E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */,
|
||||||
E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */,
|
E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */,
|
||||||
|
E10231492BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
||||||
5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */,
|
5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */,
|
||||||
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */,
|
E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */,
|
||||||
E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */,
|
E1575E7C293E77B5001665B1 /* TimerProxy.swift in Sources */,
|
||||||
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
|
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
|
||||||
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */,
|
E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */,
|
||||||
E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */,
|
E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */,
|
||||||
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
|
|
||||||
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */,
|
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */,
|
||||||
4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */,
|
||||||
E174121029AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
|
E174121029AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
|
||||||
|
@ -3740,7 +3815,7 @@
|
||||||
E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */,
|
E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */,
|
||||||
E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */,
|
||||||
E1A3E4D12BB7F5BF005C59F8 /* ErrorCard.swift in Sources */,
|
E1A3E4D12BB7F5BF005C59F8 /* ErrorCard.swift in Sources */,
|
||||||
C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */,
|
E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
|
||||||
E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */,
|
E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */,
|
||||||
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
|
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
|
||||||
E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */,
|
E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */,
|
||||||
|
@ -3778,7 +3853,6 @@
|
||||||
E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */,
|
E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */,
|
||||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||||
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */,
|
||||||
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
|
||||||
E133328829538D8D00EE76AB /* Files.swift in Sources */,
|
E133328829538D8D00EE76AB /* Files.swift in Sources */,
|
||||||
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */,
|
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */,
|
||||||
E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */,
|
E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */,
|
||||||
|
@ -3795,16 +3869,15 @@
|
||||||
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
|
||||||
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */,
|
|
||||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||||
E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */,
|
E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */,
|
||||||
|
E102314D2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */,
|
||||||
E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
||||||
E17FB55228C119D400311DFE /* Displayable.swift in Sources */,
|
E17FB55228C119D400311DFE /* Displayable.swift in Sources */,
|
||||||
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
||||||
E113132B28BDB4B500930F75 /* NavigationBarDrawerView.swift in Sources */,
|
E113132B28BDB4B500930F75 /* NavigationBarDrawerView.swift in Sources */,
|
||||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||||
E1559A76294D960C00C1FFBC /* MainOverlay.swift in Sources */,
|
E1559A76294D960C00C1FFBC /* MainOverlay.swift in Sources */,
|
||||||
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */,
|
|
||||||
E14EDECC2B8FB709000F00A4 /* ItemYear.swift in Sources */,
|
E14EDECC2B8FB709000F00A4 /* ItemYear.swift in Sources */,
|
||||||
E1DE2B4A2B97ECB900F6715F /* ErrorView.swift in Sources */,
|
E1DE2B4A2B97ECB900F6715F /* ErrorView.swift in Sources */,
|
||||||
E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */,
|
E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */,
|
||||||
|
@ -3818,7 +3891,6 @@
|
||||||
E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */,
|
E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */,
|
||||||
E129429B28F4A5E300796AC6 /* PlaybackSettingsView.swift in Sources */,
|
E129429B28F4A5E300796AC6 /* PlaybackSettingsView.swift in Sources */,
|
||||||
E1E9017B28DAAE4D001B1594 /* RoundedCorner.swift in Sources */,
|
E1E9017B28DAAE4D001B1594 /* RoundedCorner.swift in Sources */,
|
||||||
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */,
|
|
||||||
E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */,
|
E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */,
|
||||||
E18E0204288749200022598C /* RowDivider.swift in Sources */,
|
E18E0204288749200022598C /* RowDivider.swift in Sources */,
|
||||||
E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */,
|
E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */,
|
||||||
|
@ -3839,12 +3911,9 @@
|
||||||
E173DA5226D04AAF00CC4EB7 /* Color.swift in Sources */,
|
E173DA5226D04AAF00CC4EB7 /* Color.swift in Sources */,
|
||||||
E1B5784128F8AFCB00D42911 /* WrappedView.swift in Sources */,
|
E1B5784128F8AFCB00D42911 /* WrappedView.swift in Sources */,
|
||||||
E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */,
|
E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */,
|
||||||
C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */,
|
|
||||||
E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */,
|
E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */,
|
||||||
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
||||||
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
|
|
||||||
E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */,
|
E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */,
|
||||||
C49AE1182BC191BB004562B2 /* LiveTVChannelProgram.swift in Sources */,
|
|
||||||
E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
E1ED91152B95897500802036 /* LatestInLibraryViewModel.swift in Sources */,
|
||||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||||
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||||
|
@ -3853,6 +3922,7 @@
|
||||||
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
||||||
E1D37F552B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */,
|
E1D37F552B9CEF4600343D2B /* DeviceProfile+NativeProfile.swift in Sources */,
|
||||||
E129428528F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */,
|
E129428528F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */,
|
||||||
|
E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */,
|
||||||
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
||||||
E129428828F0831F00796AC6 /* SplitTimestamp.swift in Sources */,
|
E129428828F0831F00796AC6 /* SplitTimestamp.swift in Sources */,
|
||||||
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */,
|
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */,
|
||||||
|
@ -3880,6 +3950,7 @@
|
||||||
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
|
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
|
||||||
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */,
|
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */,
|
||||||
E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */,
|
E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */,
|
||||||
|
E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */,
|
||||||
E1937A61288F32DB00CB80AA /* Poster.swift in Sources */,
|
E1937A61288F32DB00CB80AA /* Poster.swift in Sources */,
|
||||||
E1CAF65F2BA345830087D991 /* MediaViewModel.swift in Sources */,
|
E1CAF65F2BA345830087D991 /* MediaViewModel.swift in Sources */,
|
||||||
E1EA9F6A28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */,
|
E1EA9F6A28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */,
|
||||||
|
@ -3894,6 +3965,7 @@
|
||||||
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
||||||
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
||||||
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
||||||
|
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
|
||||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||||
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,
|
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,
|
||||||
E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
|
E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
|
||||||
|
@ -3906,6 +3978,7 @@
|
||||||
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
|
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
|
||||||
E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */,
|
E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */,
|
||||||
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
|
||||||
|
E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
|
||||||
E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
|
E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
|
||||||
E1401CA5293813F400E8B599 /* InvertedDarkAppIcon.swift in Sources */,
|
E1401CA5293813F400E8B599 /* InvertedDarkAppIcon.swift in Sources */,
|
||||||
E1C8CE7C28FF015000DF5D7B /* TrailingTimestampType.swift in Sources */,
|
E1C8CE7C28FF015000DF5D7B /* TrailingTimestampType.swift in Sources */,
|
||||||
|
@ -3916,8 +3989,10 @@
|
||||||
E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */,
|
E1579EA72B97DC1500A31CA1 /* Eventful.swift in Sources */,
|
||||||
E1B33ED128EB860A0073B0FD /* LargePlaybackButtons.swift in Sources */,
|
E1B33ED128EB860A0073B0FD /* LargePlaybackButtons.swift in Sources */,
|
||||||
E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */,
|
E1549664296CA2EF00C4EF88 /* SwiftfinStore.swift in Sources */,
|
||||||
|
E102313F2BCF8A3C009D71FC /* WideChannelGridItem.swift in Sources */,
|
||||||
E113133228BDC72000930F75 /* FilterView.swift in Sources */,
|
E113133228BDC72000930F75 /* FilterView.swift in Sources */,
|
||||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
|
E102313D2BCF8A3C009D71FC /* ProgramsView.swift in Sources */,
|
||||||
E170D105294D21FA0017224C /* MediaSourceInfoView.swift in Sources */,
|
E170D105294D21FA0017224C /* MediaSourceInfoView.swift in Sources */,
|
||||||
E1D37F4B2B9CEA5C00343D2B /* ImageSource.swift in Sources */,
|
E1D37F4B2B9CEA5C00343D2B /* ImageSource.swift in Sources */,
|
||||||
E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */,
|
E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */,
|
||||||
|
@ -3941,6 +4016,7 @@
|
||||||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||||
E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */,
|
E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */,
|
||||||
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
|
E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */,
|
||||||
|
E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */,
|
||||||
E1DC9844296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
E1DC9844296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
||||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||||
E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */,
|
E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */,
|
||||||
|
@ -3948,7 +4024,6 @@
|
||||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||||
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */,
|
E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */,
|
||||||
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
|
|
||||||
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
|
||||||
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */,
|
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */,
|
||||||
E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */,
|
E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */,
|
||||||
|
@ -3958,7 +4033,6 @@
|
||||||
E1E750692A33E9B400B2C1EE /* MediaSourcesCard.swift in Sources */,
|
E1E750692A33E9B400B2C1EE /* MediaSourcesCard.swift in Sources */,
|
||||||
E1DE2B4C2B98389E00F6715F /* PaletteOverlayRenderingModifier.swift in Sources */,
|
E1DE2B4C2B98389E00F6715F /* PaletteOverlayRenderingModifier.swift in Sources */,
|
||||||
E18295E429CAC6F100F91ED0 /* BasicNavigationCoordinator.swift in Sources */,
|
E18295E429CAC6F100F91ED0 /* BasicNavigationCoordinator.swift in Sources */,
|
||||||
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */,
|
|
||||||
E129429328F2845000796AC6 /* SliderType.swift in Sources */,
|
E129429328F2845000796AC6 /* SliderType.swift in Sources */,
|
||||||
E113133A28BEB71D00930F75 /* FilterViewModel.swift in Sources */,
|
E113133A28BEB71D00930F75 /* FilterViewModel.swift in Sources */,
|
||||||
E1E6C44C29AED2BE0064123F /* HorizontalAlignment.swift in Sources */,
|
E1E6C44C29AED2BE0064123F /* HorizontalAlignment.swift in Sources */,
|
||||||
|
@ -3982,7 +4056,6 @@
|
||||||
E1BDF2F529524E6400CC0294 /* PlayNextItemActionButton.swift in Sources */,
|
E1BDF2F529524E6400CC0294 /* PlayNextItemActionButton.swift in Sources */,
|
||||||
E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */,
|
E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */,
|
||||||
E18ACA952A15A3E100BB4F35 /* (null) in Sources */,
|
E18ACA952A15A3E100BB4F35 /* (null) in Sources */,
|
||||||
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */,
|
|
||||||
E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */,
|
E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */,
|
||||||
E1DC983D296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
E1DC983D296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
||||||
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */,
|
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */,
|
||||||
|
@ -3999,10 +4072,10 @@
|
||||||
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||||
E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */,
|
E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */,
|
||||||
E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */,
|
||||||
|
E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.swift in Sources */,
|
||||||
E18ACA8F2A15A2CF00BB4F35 /* (null) in Sources */,
|
E18ACA8F2A15A2CF00BB4F35 /* (null) in Sources */,
|
||||||
E1401CA72938140300E8B599 /* PrimaryAppIcon.swift in Sources */,
|
E1401CA72938140300E8B599 /* PrimaryAppIcon.swift in Sources */,
|
||||||
E1937A3E288F0D3D00CB80AA /* UIScreen.swift in Sources */,
|
E1937A3E288F0D3D00CB80AA /* UIScreen.swift in Sources */,
|
||||||
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */,
|
|
||||||
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
|
||||||
E1D37F582B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift in Sources */,
|
E1D37F582B9CEF4B00343D2B /* DeviceProfile+SwiftfinProfile.swift in Sources */,
|
||||||
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */,
|
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */,
|
||||||
|
@ -4058,6 +4131,7 @@
|
||||||
E18E01EA288747230022598C /* MovieItemView.swift in Sources */,
|
E18E01EA288747230022598C /* MovieItemView.swift in Sources */,
|
||||||
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
|
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
|
||||||
E148128528C15472003B8787 /* SortOrder.swift in Sources */,
|
E148128528C15472003B8787 /* SortOrder.swift in Sources */,
|
||||||
|
E10231602BCF8B7E009D71FC /* VideoPlayerWrapperCoordinator.swift in Sources */,
|
||||||
E1D842172932AB8F00D1041A /* NativeVideoPlayer.swift in Sources */,
|
E1D842172932AB8F00D1041A /* NativeVideoPlayer.swift in Sources */,
|
||||||
E1A3E4C72BB74E50005C59F8 /* EpisodeCard.swift in Sources */,
|
E1A3E4C72BB74E50005C59F8 /* EpisodeCard.swift in Sources */,
|
||||||
E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */,
|
E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */,
|
||||||
|
|
|
@ -9,22 +9,28 @@
|
||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LandscapePosterProgressBar: View {
|
// TODO: fix relative padding, or remove?
|
||||||
|
// TODO: gradient should grow/shrink with content, not relative to container
|
||||||
|
|
||||||
|
struct LandscapePosterProgressBar<Content: View>: View {
|
||||||
|
|
||||||
@Default(.accentColor)
|
@Default(.accentColor)
|
||||||
private var accentColor
|
private var accentColor
|
||||||
|
|
||||||
let title: String
|
|
||||||
let progress: CGFloat
|
|
||||||
|
|
||||||
// Scale padding depending on view width
|
// Scale padding depending on view width
|
||||||
@State
|
@State
|
||||||
private var paddingScale: CGFloat = 1.0
|
private var paddingScale: CGFloat = 1.0
|
||||||
@State
|
@State
|
||||||
private var width: CGFloat = 0
|
private var width: CGFloat = 0
|
||||||
|
|
||||||
|
private let content: () -> Content
|
||||||
|
private let progress: Double
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
|
|
||||||
|
Color.clear
|
||||||
|
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
stops: [
|
stops: [
|
||||||
.init(color: .clear, location: 0),
|
.init(color: .clear, location: 0),
|
||||||
|
@ -37,11 +43,7 @@ struct LandscapePosterProgressBar: View {
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 3 * paddingScale) {
|
VStack(alignment: .leading, spacing: 3 * paddingScale) {
|
||||||
|
|
||||||
Spacer()
|
content()
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
|
|
||||||
ProgressBar(progress: progress)
|
ProgressBar(progress: progress)
|
||||||
.foregroundColor(accentColor)
|
.foregroundColor(accentColor)
|
||||||
|
@ -55,3 +57,43 @@ struct LandscapePosterProgressBar: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LandscapePosterProgressBar where Content == Text {
|
||||||
|
|
||||||
|
init(
|
||||||
|
title: String,
|
||||||
|
progress: Double
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
content: {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
},
|
||||||
|
progress: progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LandscapePosterProgressBar where Content == EmptyView {
|
||||||
|
|
||||||
|
init(progress: Double) {
|
||||||
|
self.init(
|
||||||
|
content: { EmptyView() },
|
||||||
|
progress: progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LandscapePosterProgressBar {
|
||||||
|
|
||||||
|
init(
|
||||||
|
progress: Double,
|
||||||
|
@ViewBuilder content: @escaping () -> Content
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
content: content,
|
||||||
|
progress: progress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import JellyfinAPI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// TODO: expose `ImageView.image` modifier for image aspect fill/fit
|
// TODO: expose `ImageView.image` modifier for image aspect fill/fit
|
||||||
|
// TODO: allow `content` to trigger `onSelect`?
|
||||||
|
|
||||||
struct PosterButton<Item: Poster>: View {
|
struct PosterButton<Item: Poster>: View {
|
||||||
|
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import CollectionVGrid
|
||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: wide + narrow view toggling
|
||||||
|
// - after `PosterType` has been refactored and with customizable toggle button
|
||||||
|
// TODO: sorting by number/filtering
|
||||||
|
// - should be able to use normal filter view model, but how to add custom filters for data context?
|
||||||
|
|
||||||
|
struct ChannelLibraryView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var mainRouter: MainCoordinator.Router
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var layout: CollectionVGridLayout
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var viewModel = ChannelLibraryViewModel()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if UIDevice.isPhone {
|
||||||
|
layout = .columns(1)
|
||||||
|
} else {
|
||||||
|
layout = .minWidth(250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentView: some View {
|
||||||
|
CollectionVGrid(
|
||||||
|
$viewModel.elements,
|
||||||
|
layout: layout
|
||||||
|
) { channel in
|
||||||
|
WideChannelGridItem(channel: channel)
|
||||||
|
.onSelect {
|
||||||
|
guard let mediaSource = channel.channel.mediaSources?.first else { return }
|
||||||
|
mainRouter.route(
|
||||||
|
to: \.liveVideoPlayer,
|
||||||
|
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReachedBottomEdge(offset: .offset(300)) {
|
||||||
|
viewModel.send(.getNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func errorView(with error: some Error) -> some View {
|
||||||
|
ErrorView(error: error)
|
||||||
|
.onRetry {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WrappedView {
|
||||||
|
switch viewModel.state {
|
||||||
|
case .content:
|
||||||
|
if viewModel.elements.isEmpty {
|
||||||
|
L10n.noResults.text
|
||||||
|
} else {
|
||||||
|
contentView
|
||||||
|
}
|
||||||
|
case let .error(error):
|
||||||
|
errorView(with: error)
|
||||||
|
case .initial, .refreshing:
|
||||||
|
DelayedProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(L10n.channels)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.onFirstAppear {
|
||||||
|
if viewModel.state == .initial {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.afterLastDisappear { interval in
|
||||||
|
// refresh after 3 hours
|
||||||
|
if interval >= 10800 {
|
||||||
|
viewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.topBarTrailing {
|
||||||
|
|
||||||
|
if viewModel.backgroundStates.contains(.gettingNextPage) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: can look busy with 3 programs, probably just do 2?
|
||||||
|
|
||||||
|
extension ChannelLibraryView {
|
||||||
|
|
||||||
|
struct WideChannelGridItem: View {
|
||||||
|
|
||||||
|
@Default(.accentColor)
|
||||||
|
private var accentColor
|
||||||
|
|
||||||
|
@Environment(\.colorScheme)
|
||||||
|
private var colorScheme
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var contentSize: CGSize = .zero
|
||||||
|
@State
|
||||||
|
private var now: Date = .now
|
||||||
|
|
||||||
|
let channel: ChannelProgram
|
||||||
|
|
||||||
|
private var onSelect: () -> Void
|
||||||
|
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
private var channelLogo: some View {
|
||||||
|
VStack {
|
||||||
|
ZStack {
|
||||||
|
Color.secondarySystemFill
|
||||||
|
.opacity(colorScheme == .dark ? 0.5 : 1)
|
||||||
|
.posterShadow()
|
||||||
|
|
||||||
|
ImageView(channel.portraitPosterImageSource(maxWidth: 80))
|
||||||
|
.image {
|
||||||
|
$0.aspectRatio(contentMode: .fit)
|
||||||
|
}
|
||||||
|
.failure {
|
||||||
|
SystemImageContentView(systemName: channel.typeSystemImage)
|
||||||
|
.background(color: .clear)
|
||||||
|
.imageFrameRatio(width: 2, height: 2)
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.padding(2)
|
||||||
|
}
|
||||||
|
.aspectRatio(1.0, contentMode: .fill)
|
||||||
|
.cornerRadius(ratio: 0.0375, of: \.width)
|
||||||
|
|
||||||
|
Text(channel.channel.number ?? "")
|
||||||
|
.font(.body)
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundColor(Color.jellyfinPurple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func programLabel(for program: BaseItemDto) -> some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
AlternateLayoutView(alignment: .leading) {
|
||||||
|
Text(Date(timeIntervalSince1970: 0), style: .time)
|
||||||
|
.monospacedDigit()
|
||||||
|
} content: {
|
||||||
|
if let startDate = program.startDate {
|
||||||
|
Text(startDate, style: .time)
|
||||||
|
.monospacedDigit()
|
||||||
|
} else {
|
||||||
|
Text(String.emptyTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(program.displayTitle)
|
||||||
|
}
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var programListView: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
if let currentProgram = channel.currentProgram {
|
||||||
|
ProgressBar(progress: currentProgram.programProgress(relativeTo: now) ?? 0)
|
||||||
|
.frame(height: 5)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
|
||||||
|
programLabel(for: currentProgram)
|
||||||
|
.font(.footnote.weight(.bold))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let nextProgram = channel.programAfterCurrent(offset: 0) {
|
||||||
|
programLabel(for: nextProgram)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let futureProgram = channel.programAfterCurrent(offset: 1) {
|
||||||
|
programLabel(for: futureProgram)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.id(channel.currentProgram)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
Button {
|
||||||
|
onSelect()
|
||||||
|
} label: {
|
||||||
|
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding) {
|
||||||
|
|
||||||
|
channelLogo
|
||||||
|
.frame(width: 80)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text(channel.displayTitle)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
if channel.programs.isNotEmpty {
|
||||||
|
programListView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.size($contentSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Color.secondarySystemFill
|
||||||
|
.frame(width: contentSize.width, height: 1)
|
||||||
|
}
|
||||||
|
.onReceive(timer) { newValue in
|
||||||
|
now = newValue
|
||||||
|
}
|
||||||
|
.animation(.linear(duration: 0.2), value: channel.currentProgram)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChannelLibraryView.WideChannelGridItem {
|
||||||
|
|
||||||
|
init(channel: ChannelProgram) {
|
||||||
|
self.init(
|
||||||
|
channel: channel,
|
||||||
|
onSelect: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onSelect(_ action: @escaping () -> Void) -> Self {
|
||||||
|
copy(modifying: \.onSelect, with: action)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,124 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 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.imageURL(.primary, 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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 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 {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
ZStack(alignment: .center) {
|
|
||||||
ImageView(channel.imageURL(.primary, maxWidth: 56))
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(width: 56, height: 56)
|
|
||||||
|
|
||||||
if loading {
|
|
||||||
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, 4)
|
|
||||||
.padding(.leading, 4)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(channel.number != nil ? "\(channel.number ?? "") " : "")
|
|
||||||
.font(.body)
|
|
||||||
.lineLimit(1)
|
|
||||||
.foregroundColor(Color.jellyfinPurple)
|
|
||||||
.frame(alignment: .leading)
|
|
||||||
.padding(.init(top: 0, leading: 0, bottom: 4, trailing: 0))
|
|
||||||
}
|
|
||||||
.padding(.top, 4)
|
|
||||||
}
|
|
||||||
.frame(alignment: .leading)
|
|
||||||
.opacity(loading ? 0.5 : 1.0)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
Text("\(channel.name ?? "")")
|
|
||||||
.font(.body)
|
|
||||||
.bold()
|
|
||||||
.lineLimit(1)
|
|
||||||
.foregroundColor(Color.jellyfinPurple)
|
|
||||||
.frame(alignment: .leading)
|
|
||||||
|
|
||||||
progressBar()
|
|
||||||
.padding(.top, 4)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text(currentProgramText.timeDisplay)
|
|
||||||
.font(.footnote)
|
|
||||||
.bold()
|
|
||||||
.lineLimit(1)
|
|
||||||
.foregroundColor(Color("TextHighlightColor"))
|
|
||||||
.frame(width: 38, alignment: .leading)
|
|
||||||
|
|
||||||
Text(currentProgramText.title)
|
|
||||||
.font(.footnote)
|
|
||||||
.bold()
|
|
||||||
.lineLimit(1)
|
|
||||||
.foregroundColor(Color("TextHighlightColor"))
|
|
||||||
}
|
|
||||||
.padding(.top, 4)
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
.padding(8)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.secondarySystemFill))
|
|
||||||
.onTapGesture {
|
|
||||||
onSelect { loadingState in
|
|
||||||
loading = loadingState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func progressBar() -> some View {
|
|
||||||
VStack(alignment: .center) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func programLabel(timeText: String, titleText: String, color: Color) -> some View {
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
Text(timeText)
|
|
||||||
.font(.footnote)
|
|
||||||
.lineLimit(1)
|
|
||||||
.foregroundColor(color)
|
|
||||||
.frame(width: 38, alignment: .leading)
|
|
||||||
Text(titleText)
|
|
||||||
.font(.footnote)
|
|
||||||
.lineLimit(1)
|
|
||||||
.foregroundColor(color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import CollectionVGrid
|
|
||||||
import Foundation
|
|
||||||
import JellyfinAPI
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
|
|
||||||
|
|
||||||
struct LiveTVChannelsView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var liveTVRouter: LiveTVCoordinator.Router
|
|
||||||
@EnvironmentObject
|
|
||||||
private var mainRouter: MainCoordinator.Router
|
|
||||||
|
|
||||||
@StateObject
|
|
||||||
var viewModel = LiveTVChannelsViewModel()
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func channelCell(for 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: { _ in
|
|
||||||
guard let mediaSource = channel.mediaSources?.first else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModel.stopScheduleCheckTimer()
|
|
||||||
mainRouter.route(to: \.liveVideoPlayer, LiveVideoPlayerManager(item: channel, mediaSource: mediaSource))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
if viewModel.isLoading {
|
|
||||||
ProgressView()
|
|
||||||
} else if viewModel.elements.isNotEmpty {
|
|
||||||
CollectionVGrid(
|
|
||||||
$viewModel.elements,
|
|
||||||
layout: .minWidth(250, itemSpacing: 16, lineSpacing: 4)
|
|
||||||
) { program in
|
|
||||||
channelCell(for: program)
|
|
||||||
}
|
|
||||||
.onReachedBottomEdge(offset: .offset(300)) {
|
|
||||||
viewModel.send(.getNextPage)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
viewModel.startScheduleCheckTimer()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
viewModel.stopScheduleCheckTimer()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
VStack {
|
|
||||||
Text(L10n.noResults)
|
|
||||||
Button {
|
|
||||||
viewModel.send(.refresh)
|
|
||||||
} label: {
|
|
||||||
Text(L10n.reload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFirstAppear {
|
|
||||||
if viewModel.state == .initial {
|
|
||||||
viewModel.send(.refresh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Stinsen
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct LiveTVHomeView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text(L10n.comingSoon)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
//
|
|
||||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
||||||
//
|
|
||||||
|
|
||||||
import Stinsen
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct LiveTVProgramsView: View {
|
|
||||||
|
|
||||||
@EnvironmentObject
|
|
||||||
private var programsRouter: LiveTVProgramsCoordinator.Router
|
|
||||||
@StateObject
|
|
||||||
var viewModel = LiveTVProgramsViewModel()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(alignment: .leading) {
|
|
||||||
if viewModel.recommendedItems.isNotEmpty {
|
|
||||||
let items = viewModel.recommendedItems
|
|
||||||
// PosterHStack(title: L10n.onNow, type: .portrait, items: items)
|
|
||||||
// .onSelect { 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.isNotEmpty {
|
|
||||||
let items = viewModel.seriesItems
|
|
||||||
// PosterHStack(title: L10n.tvShows, type: .portrait, items: items)
|
|
||||||
// .onSelect { 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.isNotEmpty {
|
|
||||||
let items = viewModel.movieItems
|
|
||||||
// PosterHStack(title: L10n.movies, type: .portrait, items: items)
|
|
||||||
// .onSelect { 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.isNotEmpty {
|
|
||||||
let items = viewModel.sportsItems
|
|
||||||
// PosterHStack(title: L10n.sports, type: .portrait, items: items)
|
|
||||||
// .onSelect { 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.isNotEmpty {
|
|
||||||
let items = viewModel.kidsItems
|
|
||||||
// PosterHStack(title: L10n.kids, type: .portrait, items: items)
|
|
||||||
// .onSelect { 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.isNotEmpty {
|
|
||||||
let items = viewModel.newsItems
|
|
||||||
// PosterHStack(title: L10n.news, type: .portrait, items: items)
|
|
||||||
// .onSelect { 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)
|
|
||||||
//// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -93,7 +93,7 @@ extension PagingLibraryView {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
Text(item.displayTitle)
|
Text(item.displayTitle)
|
||||||
.font(posterType == .landscape ? .subheadline : .callout)
|
.font(posterType == .landscape ? .subheadline : .callout)
|
||||||
.fontWeight(.regular)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ProgramsView {
|
||||||
|
|
||||||
|
struct ProgramButtonContent: View {
|
||||||
|
|
||||||
|
let program: BaseItemDto
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
Text(program.channelName ?? .emptyDash)
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.backport
|
||||||
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
|
||||||
|
Text(program.displayTitle)
|
||||||
|
.font(.footnote.weight(.regular))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.backport
|
||||||
|
.lineLimit(1, reservesSpace: true)
|
||||||
|
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
if let startDate = program.startDate {
|
||||||
|
Text(startDate, style: .time)
|
||||||
|
} else {
|
||||||
|
Text(String.emptyDash)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("-")
|
||||||
|
|
||||||
|
if let endDate = program.endDate {
|
||||||
|
Text(endDate, style: .time)
|
||||||
|
} else {
|
||||||
|
Text(String.emptyDash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension ProgramsView {
|
||||||
|
|
||||||
|
struct ProgramProgressOverlay: View {
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var programProgress: Double = 0.0
|
||||||
|
|
||||||
|
let program: BaseItemDto
|
||||||
|
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WrappedView {
|
||||||
|
if let startDate = program.startDate, startDate < Date.now {
|
||||||
|
LandscapePosterProgressBar(
|
||||||
|
progress: program.programProgress ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(timer) { newValue in
|
||||||
|
if let startDate = program.startDate, startDate < newValue, let duration = program.programDuration {
|
||||||
|
programProgress = newValue.timeIntervalSince(startDate) / duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
//
|
||||||
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// TODO: background refresh for programs with timer?
|
||||||
|
// TODO: find other another way to handle channels/other views?
|
||||||
|
|
||||||
|
// Note: there are some unsafe first element accesses, but `ChannelProgram` data should always have a single program
|
||||||
|
|
||||||
|
struct ProgramsView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var mainRouter: MainCoordinator.Router
|
||||||
|
@EnvironmentObject
|
||||||
|
private var router: LiveTVCoordinator.Router
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var programsViewModel = ProgramsViewModel()
|
||||||
|
|
||||||
|
private func errorView(with error: some Error) -> some View {
|
||||||
|
ErrorView(error: error)
|
||||||
|
.onRetry {
|
||||||
|
programsViewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var liveTVSectionScrollView: some View {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack {
|
||||||
|
liveTVSectionPill(
|
||||||
|
title: L10n.channels,
|
||||||
|
systemImage: "play.square.stack"
|
||||||
|
) {
|
||||||
|
router.route(to: \.channels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edgePadding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: probably make own pill view
|
||||||
|
// - see if could merge with item view pills
|
||||||
|
private func liveTVSectionPill(title: String, systemImage: String, onSelect: @escaping () -> Void) -> some View {
|
||||||
|
Button {
|
||||||
|
onSelect()
|
||||||
|
} label: {
|
||||||
|
Label(title, systemImage: systemImage)
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.padding(8)
|
||||||
|
.background {
|
||||||
|
Color.systemFill
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentView: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
|
||||||
|
liveTVSectionScrollView
|
||||||
|
|
||||||
|
if programsViewModel.hasNoResults {
|
||||||
|
// TODO: probably change to "No Programs"
|
||||||
|
L10n.noResults.text
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.recommended.isNotEmpty {
|
||||||
|
programsSection(title: L10n.onNow, keyPath: \.recommended)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.series.isNotEmpty {
|
||||||
|
programsSection(title: L10n.series, keyPath: \.series)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.movies.isNotEmpty {
|
||||||
|
programsSection(title: L10n.movies, keyPath: \.movies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.kids.isNotEmpty {
|
||||||
|
programsSection(title: L10n.kids, keyPath: \.kids)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.sports.isNotEmpty {
|
||||||
|
programsSection(title: L10n.sports, keyPath: \.sports)
|
||||||
|
}
|
||||||
|
|
||||||
|
if programsViewModel.news.isNotEmpty {
|
||||||
|
programsSection(title: L10n.news, keyPath: \.news)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func programsSection(
|
||||||
|
title: String,
|
||||||
|
keyPath: KeyPath<ProgramsViewModel, [ChannelProgram]>
|
||||||
|
) -> some View {
|
||||||
|
PosterHStack(
|
||||||
|
title: title,
|
||||||
|
type: .landscape,
|
||||||
|
items: programsViewModel[keyPath: keyPath]
|
||||||
|
)
|
||||||
|
.content {
|
||||||
|
ProgramButtonContent(program: $0.programs[0])
|
||||||
|
}
|
||||||
|
.imageOverlay {
|
||||||
|
ProgramProgressOverlay(program: $0.programs[0])
|
||||||
|
}
|
||||||
|
.onSelect { channelProgram in
|
||||||
|
guard let mediaSource = channelProgram.channel.mediaSources?.first else { return }
|
||||||
|
mainRouter.route(
|
||||||
|
to: \.liveVideoPlayer,
|
||||||
|
LiveVideoPlayerManager(item: channelProgram.channel, mediaSource: mediaSource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
WrappedView {
|
||||||
|
switch programsViewModel.state {
|
||||||
|
case .content:
|
||||||
|
contentView
|
||||||
|
case let .error(error):
|
||||||
|
errorView(with: error)
|
||||||
|
case .initial, .refreshing:
|
||||||
|
DelayedProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(L10n.liveTV)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.onFirstAppear {
|
||||||
|
if programsViewModel.state == .initial {
|
||||||
|
programsViewModel.send(.refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,9 +40,6 @@ struct LiveNativeVideoPlayer: View {
|
||||||
.navigationBarHidden()
|
.navigationBarHidden()
|
||||||
.statusBarHidden()
|
.statusBarHidden()
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.onDisappear {
|
|
||||||
NotificationCenter.default.post(name: .livePlayerDismissed, object: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -170,9 +170,6 @@ struct LiveVideoPlayer: View {
|
||||||
gestureStateHandler: gestureStateHandler,
|
gestureStateHandler: gestureStateHandler,
|
||||||
updateViewProxy: updateViewProxy
|
updateViewProxy: updateViewProxy
|
||||||
)
|
)
|
||||||
.onDisappear {
|
|
||||||
NotificationCenter.default.post(name: .livePlayerDismissed, object: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -221,9 +218,6 @@ struct LiveVideoPlayer: View {
|
||||||
audioOffset = 0
|
audioOffset = 0
|
||||||
subtitleOffset = 0
|
subtitleOffset = 0
|
||||||
}
|
}
|
||||||
.onDisappear {
|
|
||||||
NotificationCenter.default.post(name: .livePlayerDismissed, object: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue