jellyflood/Swiftfin/Views/ChannelLibraryView/Component/WideChannelGridItem.swift

170 lines
5.5 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) 2024 Jellyfin & Jellyfin Contributors
//
import Defaults
import JellyfinAPI
import SwiftUI
// TODO: can look busy with 3 programs, probably just do 2?
extension ChannelLibraryView {
struct WideChannelGridItem: View {
@Default(.accentColor)
private var accentColor
@Environment(\.colorScheme)
private var colorScheme
@State
private var contentSize: CGSize = .zero
@State
private var now: Date = .now
let channel: ChannelProgram
private var onSelect: () -> Void
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
private var channelLogo: some View {
VStack {
ZStack {
Color.secondarySystemFill
.opacity(colorScheme == .dark ? 0.5 : 1)
.posterShadow()
ImageView(channel.portraitPosterImageSource(maxWidth: 80))
.image {
$0.aspectRatio(contentMode: .fit)
}
.failure {
SystemImageContentView(systemName: channel.typeSystemImage)
.background(color: .clear)
.imageFrameRatio(width: 2, height: 2)
}
.placeholder {
EmptyView()
}
.padding(2)
}
.aspectRatio(1.0, contentMode: .fill)
.cornerRadius(ratio: 0.0375, of: \.width)
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(Date(timeIntervalSince1970: 0), style: .time)
.monospacedDigit()
} content: {
if let startDate = program.startDate {
Text(startDate, style: .time)
.monospacedDigit()
} else {
Text(String.emptyTime)
}
}
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 {
onSelect()
} label: {
HStack(alignment: .center, spacing: EdgeInsets.defaultEdgePadding) {
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)
.size($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)
}
}
}
extension ChannelLibraryView.WideChannelGridItem {
init(channel: ChannelProgram) {
self.init(
channel: channel,
onSelect: {}
)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}