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
|
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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue