From 8bc87282eec97dedd4df414cdd12b3e93326ae40 Mon Sep 17 00:00:00 2001 From: jhays Date: Sun, 1 May 2022 21:21:06 -0500 Subject: [PATCH] update live tv cells for tvOS --- .../Coordinators/LibraryListCoordinator.swift | 14 +- .../Views/LiveTVChannelItemElement.swift | 130 +++++++++++------- Swiftfin tvOS/Views/LiveTVChannelsView.swift | 80 ++++++++--- 3 files changed, 152 insertions(+), 72 deletions(-) diff --git a/Shared/Coordinators/LibraryListCoordinator.swift b/Shared/Coordinators/LibraryListCoordinator.swift index da36cc92..5ac1ad91 100644 --- a/Shared/Coordinators/LibraryListCoordinator.swift +++ b/Shared/Coordinators/LibraryListCoordinator.swift @@ -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 { diff --git a/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift b/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift index f36aa1bf..90dc1727 100644 --- a/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift +++ b/Swiftfin tvOS/Views/LiveTVChannelItemElement.swift @@ -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) + } + } } diff --git a/Swiftfin tvOS/Views/LiveTVChannelsView.swift b/Swiftfin tvOS/Views/LiveTVChannelsView.swift index 19d66649..050f8833 100644 --- a/Swiftfin tvOS/Views/LiveTVChannelsView.swift +++ b/Swiftfin tvOS/Views/LiveTVChannelsView.swift @@ -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) + } }