121 lines
3.9 KiB
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)
|
|
)
|
|
}
|
|
}
|