jellyflood/jellyflood tvOS/Views/ProgramGuideView/Components/EPGProgramCell.swift

121 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 JellyfinAPI
import SwiftUI
struct EPGProgramCell: View {
@EnvironmentObject
private var router: VideoPlayerWrapperCoordinator.Router
@Environment(\.isFocused)
private var isFocused
let program: BaseItemDto
let channel: BaseItemDto
let cellWidth: CGFloat
let isCurrentlyAiring: Bool
@State
private var currentTime = Date.now
private let timer = Timer.publish(every: 30, on: .main, in: .common).autoconnect()
var body: some View {
Button {
handleSelection()
} label: {
ZStack(alignment: .leading) {
// Background
RoundedRectangle(cornerRadius: 8)
.fill(backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(borderColor, lineWidth: isFocused ? 4 : 2)
)
// Progress indicator for currently airing programs
if isCurrentlyAiring, let progress = program.programProgress(relativeTo: currentTime) {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 8)
.fill(Color.accentColor.opacity(0.3))
.frame(width: geometry.size.width * progress)
}
}
// Content
VStack(alignment: .leading, spacing: 4) {
Text(program.displayTitle)
.font(.callout)
.fontWeight(isFocused ? .semibold : .regular)
.lineLimit(2)
.foregroundColor(isFocused ? .black : .white)
if let startDate = program.startDate,
let endDate = program.endDate
{
HStack(spacing: 4) {
Text(startDate, style: .time)
Text("-")
Text(endDate, style: .time)
}
.font(.caption2)
.foregroundColor(isFocused ? .black.opacity(0.7) : .secondary)
}
if isCurrentlyAiring {
Text("Live")
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(isFocused ? .black : .red)
}
}
.padding(8)
}
}
.frame(width: cellWidth, height: 100)
.buttonStyle(.card)
.onReceive(timer) { _ in
currentTime = Date.now
}
}
private var backgroundColor: Color {
if isFocused {
return .white
} else if isCurrentlyAiring {
return Color.accentColor.opacity(0.2)
} else {
return Color(white: 0.2)
}
}
private var borderColor: Color {
if isFocused {
return .white
} else if isCurrentlyAiring {
return .accentColor
} else {
return Color(white: 0.3)
}
}
private func handleSelection() {
// For Live TV from EPG, we play the channel
// If program is currently airing, playback will start from current position
// If program is in the future, channel will start playing whatever is currently on
guard let mediaSource = channel.mediaSources?.first else { return }
router.route(
to: \.liveVideoPlayer,
LiveVideoPlayerManager(item: channel, mediaSource: mediaSource)
)
}
}