// // 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) } } } }