// // 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) 2022 Jellyfin & Jellyfin Contributors // import JellyfinAPI import SwiftUI struct LiveTVChannelItemElement: View { @FocusState private var focused: Bool @State private var loading: Bool = false @State private var isFocused: Bool = false var channel: BaseItemDto var currentProgram: BaseItemDto? var currentProgramText: LiveTVChannelViewProgram var nextProgramsText: [LiveTVChannelViewProgram] var onSelect: (@escaping (Bool) -> Void) -> Void var progressPercent: Double { if let currentProgram = currentProgram { let progressPercent = currentProgram.getLiveProgressPercentage() if progressPercent > 1.0 { return 1.0 } else { return progressPercent } } return 0 } private var detailText: String { guard let program = currentProgram else { return "" } var text = "" if let season = program.parentIndexNumber, let episode = program.indexNumber { text.append("\(season)x\(episode) ") } else if let episode = program.indexNumber { text.append("\(episode) ") } if let title = program.episodeTitle { text.append("\(title) ") } if let year = program.productionYear { text.append("\(year) ") } if let rating = program.officialRating { text.append("\(rating)") } return text } var body: some View { ZStack { VStack { HStack { Text(channel.number ?? "") .font(.footnote) .frame(alignment: .leading) .padding() Spacer() }.frame(alignment: .top) Spacer() } GeometryReader { gp in VStack { ImageView(channel.imageSource(.primary, maxWidth: 192)) .aspectRatio(contentMode: .fit) .frame(width: 192, alignment: .center) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .padding(.init(top: 16, leading: 8, bottom: gp.size.height / 2, trailing: 0)) VStack { Text(channel.name ?? "?") .font(.footnote) .lineLimit(1) .frame(alignment: .center) .foregroundColor(Color.jellyfinPurple) .padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0)) programLabel( timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title, color: Color("TextHighlightColor"), font: Font.system(size: 20, weight: .bold, design: .default) ) if !nextProgramsText.isEmpty, let nextItem = nextProgramsText[0] { programLabel( timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray, font: Font.system(size: 20, design: .default) ) } if nextProgramsText.count > 1, let nextItem2 = nextProgramsText[1] { programLabel( timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray, font: Font.system(size: 20, design: .default) ) } } .frame(maxHeight: .infinity, alignment: .top) .padding(.init(top: gp.size.height / 2, leading: 16, bottom: 56, trailing: 16)) .opacity(loading ? 0.5 : 1.0) } if loading { ProgressView() } VStack { GeometryReader { gp in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 6) .fill(Color.gray) .opacity(0.4) .frame(minWidth: 100, maxWidth: .infinity, minHeight: 8, maxHeight: 8) RoundedRectangle(cornerRadius: 6) .fill(Color.jellyfinPurple) .frame(width: CGFloat(progressPercent * gp.size.width), height: 8) } .frame(maxHeight: .infinity, alignment: .bottom) .padding(.init(top: 0, leading: 16, bottom: 32, trailing: 16)) } } } .overlay( RoundedRectangle(cornerRadius: 20) .stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4) ) .cornerRadius(20) .scaleEffect(isFocused ? 1.1 : 1) .focusable(true) .focused($focused) .onChange(of: focused) { foc in withAnimation(.linear(duration: 0.15)) { self.isFocused = foc } } .onLongPressGesture(minimumDuration: 0.01, pressing: { _ in }) { onSelect { loadingState in loading = loadingState } } } @ViewBuilder func programLabel(timeText: String, titleText: String, color: Color, font: Font) -> some View { HStack(alignment: .top, spacing: 4) { Text(timeText) .font(font) .lineLimit(1) .foregroundColor(color) .frame(width: 54, alignment: .leading) Text(titleText) .font(font) .lineLimit(2) .foregroundColor(color) .frame(maxWidth: .infinity, alignment: .leading) } } }