update live tv cells for tvOS

This commit is contained in:
jhays 2022-05-01 21:21:06 -05:00
parent 80477c4bbd
commit 8bc87282ee
3 changed files with 152 additions and 72 deletions

View File

@ -20,8 +20,10 @@ final class LibraryListCoordinator: NavigationCoordinatable {
var search = makeSearch var search = makeSearch
@Route(.push) @Route(.push)
var library = makeLibrary var library = makeLibrary
@Route(.push) #if os(iOS)
var liveTV = makeLiveTV @Route(.push)
var liveTV = makeLiveTV
#endif
let viewModel: LibraryListViewModel let viewModel: LibraryListViewModel
@ -37,9 +39,11 @@ final class LibraryListCoordinator: NavigationCoordinatable {
SearchCoordinator(viewModel: viewModel) SearchCoordinator(viewModel: viewModel)
} }
func makeLiveTV() -> LiveTVCoordinator { #if os(iOS)
LiveTVCoordinator() func makeLiveTV() -> LiveTVCoordinator {
} LiveTVCoordinator()
}
#endif
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {

View File

@ -18,14 +18,25 @@ struct LiveTVChannelItemElement: View {
private var isFocused: Bool = false private var isFocused: Bool = false
var channel: BaseItemDto var channel: BaseItemDto
var program: BaseItemDto? var currentProgram: BaseItemDto?
var startString = " " var currentProgramText: LiveTVChannelViewProgram
var endString = " " var nextProgramsText: [LiveTVChannelViewProgram]
var progressPercent = Double(0)
var onSelect: (@escaping (Bool) -> Void) -> Void 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 { private var detailText: String {
guard let program = program else { guard let program = currentProgram else {
return "" return ""
} }
var text = "" var text = ""
@ -60,61 +71,62 @@ struct LiveTVChannelItemElement: View {
}.frame(alignment: .top) }.frame(alignment: .top)
Spacer() Spacer()
} }
VStack {
ImageView(channel.getPrimaryImage(maxWidth: 128))
.aspectRatio(contentMode: .fit)
.frame(width: 128, alignment: .center)
.padding(.init(top: 8, leading: 0, bottom: 0, trailing: 0))
Text(channel.name ?? "?")
.font(.footnote)
.lineLimit(1)
.frame(alignment: .center)
Text(program?.name ?? L10n.notAvailableSlash)
.font(.body)
.lineLimit(1)
.foregroundColor(.green)
Text(detailText)
.font(.body)
.lineLimit(1)
.foregroundColor(.green)
Spacer()
HStack(alignment: .bottom) {
VStack {
Spacer()
HStack {
Text(startString)
.font(.footnote)
.lineLimit(1)
.frame(alignment: .leading)
Spacer() GeometryReader { gp in
VStack {
ImageView(channel.getPrimaryImage(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))
Text(endString) programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title,
.font(.footnote) color: Color("TextHighlightColor"), font: Font.system(size: 20, weight: .bold, design: .default))
.lineLimit(1) if !nextProgramsText.isEmpty,
.frame(alignment: .trailing) let nextItem = nextProgramsText[0]
} {
GeometryReader { gp in programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray,
ZStack(alignment: .leading) { font: Font.system(size: 20, design: .default))
RoundedRectangle(cornerRadius: 6) }
.fill(Color.gray) if nextProgramsText.count > 1,
.opacity(0.4) let nextItem2 = nextProgramsText[1]
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) {
RoundedRectangle(cornerRadius: 6) programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray,
.fill(Color.jellyfinPurple) font: Font.system(size: 20, design: .default))
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
}
.frame(alignment: .bottom)
}
} }
} }
.frame(maxHeight: .infinity, alignment: .top)
.padding(.init(top: gp.size.height / 2, leading: 16, bottom: 56, trailing: 16))
.opacity(loading ? 0.5 : 1.0)
} }
.padding()
.opacity(loading ? 0.5 : 1.0)
if loading { if loading {
ProgressView() 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) .overlay(RoundedRectangle(cornerRadius: 20)
.stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4)) .stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4))
@ -133,4 +145,20 @@ struct LiveTVChannelItemElement: View {
} }
} }
} }
@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)
}
}
} }

View File

@ -11,6 +11,8 @@ import JellyfinAPI
import SwiftUI import SwiftUI
import SwiftUICollection import SwiftUICollection
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
struct LiveTVChannelsView: View { struct LiveTVChannelsView: View {
@EnvironmentObject @EnvironmentObject
var router: LiveTVChannelsCoordinator.Router var router: LiveTVChannelsCoordinator.Router
@ -53,23 +55,30 @@ struct LiveTVChannelsView: View {
func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View { func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
let item = cell.item let item = cell.item
let channel = item.channel let channel = item.channel
if channel.type != "Folder" { let currentProgramDisplayText = item.currentProgram?
let progressPercent = item.program?.getLiveProgressPercentage() ?? 0 .programDisplayText(timeFormatter: viewModel.timeFormatter) ?? LiveTVChannelViewProgram(timeDisplay: "", title: "")
LiveTVChannelItemElement(channel: channel, let nextItems = item.programs.filter { program in
program: item.program, guard let start = program.startDate else {
startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ", return false
endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ", }
progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent, guard let currentStart = item.currentProgram?.startDate else {
onSelect: { loadingAction in return false
loadingAction(true) }
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in return start > currentStart
self.router.route(to: \.videoPlayer, playerViewModel)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
loadingAction(false)
}
}
})
} }
LiveTVChannelItemElement(channel: channel,
currentProgram: item.currentProgram,
currentProgramText: currentProgramDisplayText,
nextProgramsText: nextProgramsDisplayText(nextItems: nextItems, timeFormatter: viewModel.timeFormatter),
onSelect: { loadingAction in
loadingAction(true)
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
self.router.route(to: \.videoPlayer, playerViewModel)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
loadingAction(false)
}
}
})
} }
private func createGridLayout() -> NSCollectionLayoutSection { private func createGridLayout() -> NSCollectionLayoutSection {
@ -100,4 +109,43 @@ struct LiveTVChannelsView: View {
return section return section
} }
private func nextProgramsDisplayText(nextItems: [BaseItemDto], timeFormatter: DateFormatter) -> [LiveTVChannelViewProgram] {
var programsDisplayText: [LiveTVChannelViewProgram] = []
for item in nextItems {
programsDisplayText.append(item.programDisplayText(timeFormatter: timeFormatter))
}
return programsDisplayText
}
}
private extension BaseItemDto {
func programDisplayText(timeFormatter: DateFormatter) -> LiveTVChannelViewProgram {
var timeText = ""
if let start = self.startDate {
timeText.append(timeFormatter.string(from: start) + " ")
}
var displayText = ""
if let season = self.parentIndexNumber,
let episode = self.indexNumber
{
displayText.append("\(season)x\(episode) ")
} else if let episode = self.indexNumber {
displayText.append("\(episode) ")
}
if let name = self.name {
displayText.append("\(name) ")
}
if let title = self.episodeTitle {
displayText.append("\(title) ")
}
if let year = self.productionYear {
displayText.append("\(year) ")
}
if let rating = self.officialRating {
displayText.append("\(rating)")
}
return LiveTVChannelViewProgram(timeDisplay: timeText, title: displayText)
}
} }