jellyflood/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift

135 lines
4.3 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 Defaults
import JellyfinAPI
import SwiftUI
// TODO: can look busy with 3 programs, probably just do 2?
extension ChannelLibraryView {
struct DetailedChannelView: View {
@Default(.accentColor)
private var accentColor
@State
private var contentSize: CGSize = .zero
@State
private var now: Date = .now
let channel: ChannelProgram
let action: () -> Void
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
@ViewBuilder
private var channelLogo: some View {
VStack {
PosterImage(
item: channel.channel,
type: .square
)
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("00:00 AAA")
.monospacedDigit()
} content: {
if let startDate = program.startDate {
Text(startDate, style: .time)
.monospacedDigit()
} else {
Text(String.emptyRuntime)
}
}
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(action: action) {
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
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)
.trackingSize($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)
}
}
}