jellyflood/jellyflood tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift

112 lines
3.9 KiB
Swift

//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import CollectionVGrid
import Foundation
import JellyfinAPI
import Logging
import SwiftUI
struct ChannelLibraryView: View {
@EnvironmentObject
private var router: VideoPlayerWrapperCoordinator.Router
@StateObject
private var viewModel = ChannelLibraryViewModel()
@State
private var errorMessage: String?
private let logger = Logger(label: "ChannelLibraryView")
@ViewBuilder
private var contentView: some View {
CollectionVGrid(
uniqueElements: viewModel.elements,
layout: .columns(3, insets: .init(0), itemSpacing: 25, lineSpacing: 25)
) { channel in
WideChannelGridItem(channel: channel)
.onSelect {
print("🔴 CHANNEL CLICKED: \(channel.displayTitle)")
logger.info("Channel clicked: \(channel.displayTitle)")
logger.info("MediaSources count: \(channel.channel.mediaSources?.count ?? 0)")
guard let mediaSource = channel.channel.mediaSources?.first else {
let error = "No media source available for channel '\(channel.displayTitle)'"
print("🔴 ERROR: \(error)")
logger.error("\(error)")
errorMessage = error
return
}
print("🔴 MediaSource path: \(mediaSource.path ?? "nil")")
print("🔴 MediaSource transcodingURL: \(mediaSource.transcodingURL ?? "nil")")
logger.info("MediaSource ID: \(mediaSource.id ?? "nil")")
logger.info("MediaSource path: \(mediaSource.path ?? "nil")")
logger.info("MediaSource transcodingURL: \(mediaSource.transcodingURL ?? "nil")")
logger.info("MediaSource supportsDirectPlay: \(mediaSource.isSupportsDirectPlay ?? false)")
logger.info("MediaSource container: \(mediaSource.container ?? "nil")")
logger.info("Routing to live video player...")
print("🔴 ROUTING TO LIVE VIDEO PLAYER NOW")
router.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channel.channel, mediaSource: mediaSource)
)
}
}
.onReachedBottomEdge(offset: .offset(300)) {
viewModel.send(.getNextPage)
}
}
var body: some View {
ZStack {
Color(red: 0.15, green: 0.05, blue: 0.1)
.ignoresSafeArea()
switch viewModel.state {
case .content:
if viewModel.elements.isEmpty {
L10n.noResults.text
} else {
contentView
}
case let .error(error):
ErrorView(error: error)
.onRetry {
viewModel.send(.refresh)
}
case .initial, .refreshing:
ProgressView()
}
}
.animation(.linear(duration: 0.1), value: viewModel.state)
.ignoresSafeArea()
.alert("Channel Playback Error", isPresented: .constant(errorMessage != nil), presenting: errorMessage) { _ in
Button("OK") {
errorMessage = nil
}
} message: { message in
Text(message)
}
.onFirstAppear {
if viewModel.state == .initial {
viewModel.send(.refresh)
}
}
.sinceLastDisappear { interval in
// refresh after 3 hours
if interval >= 10800 {
viewModel.send(.refresh)
}
}
}
}