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
@Route(.push)
var library = makeLibrary
@Route(.push)
var liveTV = makeLiveTV
#if os(iOS)
@Route(.push)
var liveTV = makeLiveTV
#endif
let viewModel: LibraryListViewModel
@ -37,9 +39,11 @@ final class LibraryListCoordinator: NavigationCoordinatable {
SearchCoordinator(viewModel: viewModel)
}
func makeLiveTV() -> LiveTVCoordinator {
LiveTVCoordinator()
}
#if os(iOS)
func makeLiveTV() -> LiveTVCoordinator {
LiveTVCoordinator()
}
#endif
@ViewBuilder
func makeStart() -> some View {

View File

@ -18,14 +18,25 @@ struct LiveTVChannelItemElement: View {
private var isFocused: Bool = false
var channel: BaseItemDto
var program: BaseItemDto?
var startString = " "
var endString = " "
var progressPercent = Double(0)
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 = program else {
guard let program = currentProgram else {
return ""
}
var text = ""
@ -60,61 +71,62 @@ struct LiveTVChannelItemElement: View {
}.frame(alignment: .top)
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)
.font(.footnote)
.lineLimit(1)
.frame(alignment: .trailing)
}
GeometryReader { gp in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 6)
.fill(Color.gray)
.opacity(0.4)
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12)
RoundedRectangle(cornerRadius: 6)
.fill(Color.jellyfinPurple)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
}
.frame(alignment: .bottom)
}
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)
}
.padding()
.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))
@ -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 SwiftUICollection
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
struct LiveTVChannelsView: View {
@EnvironmentObject
var router: LiveTVChannelsCoordinator.Router
@ -53,23 +55,30 @@ struct LiveTVChannelsView: View {
func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
let item = cell.item
let channel = item.channel
if channel.type != "Folder" {
let progressPercent = item.program?.getLiveProgressPercentage() ?? 0
LiveTVChannelItemElement(channel: channel,
program: item.program,
startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ",
endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ",
progressPercent: progressPercent > 1.0 ? 1.0 : progressPercent,
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)
}
}
})
let currentProgramDisplayText = item.currentProgram?
.programDisplayText(timeFormatter: viewModel.timeFormatter) ?? LiveTVChannelViewProgram(timeDisplay: "", title: "")
let nextItems = item.programs.filter { program in
guard let start = program.startDate else {
return false
}
guard let currentStart = item.currentProgram?.startDate else {
return false
}
return start > currentStart
}
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 {
@ -100,4 +109,43 @@ struct LiveTVChannelsView: View {
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)
}
}