update live tv cells for tvOS
This commit is contained in:
parent
80477c4bbd
commit
8bc87282ee
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue