// // 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 extension ChannelLibraryView { struct WideChannelGridItem: View { @Default(.accentColor) private var accentColor @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() @ViewBuilder private var channelLogo: some View { VStack { ZStack { Color.clear ImageView(channel.portraitImageSources(maxWidth: 110)) .image { $0.aspectRatio(contentMode: .fit) } .failure { SystemImageContentView(systemName: channel.systemImage, ratio: 0.66) .background(color: .clear) } .placeholder { _ in EmptyView() } } .aspectRatio(1.0, contentMode: .fit) Text(channel.channel.number ?? "") .font(.body) .lineLimit(1) .foregroundStyle(.primary) } } @ViewBuilder private func programLabel(for program: BaseItemDto) -> some View { HStack(alignment: .top, spacing: EdgeInsets.edgePadding / 2) { AlternateLayoutView(alignment: .leading) { Text("00:00 AM") .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: 8) .padding(.bottom, 8) .foregroundStyle(accentColor) programLabel(for: currentProgram) .font(.caption.weight(.bold)) } if let nextProgram = channel.programAfterCurrent(offset: 0) { programLabel(for: nextProgram) .font(.caption) .foregroundStyle(.secondary) } if let futureProgram = channel.programAfterCurrent(offset: 1) { programLabel(for: futureProgram) .font(.caption) .foregroundStyle(.secondary) } } .id(channel.currentProgram) } var body: some View { Button { onSelect() } label: { HStack(alignment: .center, spacing: EdgeInsets.edgePadding / 2) { channelLogo .frame(width: 110) HStack { VStack(alignment: .leading, spacing: 5) { Text(channel.displayTitle) .font(.body) .fontWeight(.bold) .lineLimit(1) .foregroundStyle(.primary) if channel.programs.isNotEmpty { programListView } } Spacer() } .frame(maxWidth: .infinity) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, EdgeInsets.edgePadding / 2) } .buttonStyle(.card) .frame(height: 200) .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) } }