swiftformat

This commit is contained in:
jhays 2022-04-28 14:29:43 -05:00
parent a8f8a93efc
commit 80477c4bbd
13 changed files with 1684 additions and 1711 deletions

View File

@ -20,8 +20,8 @@ final class LibraryListCoordinator: NavigationCoordinatable {
var search = makeSearch var search = makeSearch
@Route(.push) @Route(.push)
var library = makeLibrary var library = makeLibrary
@Route(.push) @Route(.push)
var liveTV = makeLiveTV var liveTV = makeLiveTV
let viewModel: LibraryListViewModel let viewModel: LibraryListViewModel
@ -36,10 +36,10 @@ final class LibraryListCoordinator: NavigationCoordinatable {
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel) SearchCoordinator(viewModel: viewModel)
} }
func makeLiveTV() -> LiveTVCoordinator { func makeLiveTV() -> LiveTVCoordinator {
LiveTVCoordinator() LiveTVCoordinator()
} }
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {

View File

@ -24,7 +24,7 @@ final class LiveTVChannelsCoordinator: NavigationCoordinatable {
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
NavigationViewCoordinator(ItemCoordinator(item: item)) NavigationViewCoordinator(ItemCoordinator(item: item))
} }
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> { func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> {
NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel)) NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel))
} }

View File

@ -12,19 +12,19 @@ import Stinsen
import SwiftUI import SwiftUI
final class LiveTVCoordinator: NavigationCoordinatable { final class LiveTVCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVCoordinator.start) let stack = NavigationStack(initial: \LiveTVCoordinator.start)
@Root @Root
var start = makeStart var start = makeStart
@Route(.fullScreen) @Route(.fullScreen)
var videoPlayer = makeVideoPlayer var videoPlayer = makeVideoPlayer
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
LiveTVChannelsView() LiveTVChannelsView()
} }
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> { func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> {
NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel)) NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel))
} }
} }

View File

@ -13,28 +13,28 @@ import Stinsen
import SwiftUI import SwiftUI
final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable { final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVVideoPlayerCoordinator.start) let stack = NavigationStack(initial: \LiveTVVideoPlayerCoordinator.start)
@Root @Root
var start = makeStart var start = makeStart
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel
init(viewModel: VideoPlayerViewModel) { init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
if Defaults[.Experimental.liveTVNativePlayer] { if Defaults[.Experimental.liveTVNativePlayer] {
LiveTVNativePlayerView(viewModel: viewModel) LiveTVNativePlayerView(viewModel: viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea() .ignoresSafeArea()
} else { } else {
LiveTVPlayerView(viewModel: viewModel) LiveTVPlayerView(viewModel: viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea() .ignoresSafeArea()
} }
} }
} }

View File

@ -21,7 +21,7 @@ struct LiveTVChannelProgram: Hashable {
let id = UUID() let id = UUID()
let channel: BaseItemDto let channel: BaseItemDto
let currentProgram: BaseItemDto? let currentProgram: BaseItemDto?
let programs: [BaseItemDto] let programs: [BaseItemDto]
} }
final class LiveTVChannelsViewModel: ViewModel { final class LiveTVChannelsViewModel: ViewModel {

View File

@ -16,17 +16,17 @@ struct LibraryListView: View {
var libraryListRouter: LibraryListCoordinator.Router var libraryListRouter: LibraryListCoordinator.Router
@StateObject @StateObject
var viewModel = LibraryListViewModel() var viewModel = LibraryListViewModel()
@Default(.Experimental.liveTVAlphaEnabled) @Default(.Experimental.liveTVAlphaEnabled)
var liveTVAlphaEnabled var liveTVAlphaEnabled
var supportedCollectionTypes: [String] { var supportedCollectionTypes: [String] {
if liveTVAlphaEnabled { if liveTVAlphaEnabled {
return ["movies", "tvshows", "livetv", "boxsets", "other"] return ["movies", "tvshows", "livetv", "boxsets", "other"]
} else { } else {
return ["movies", "tvshows", "boxsets", "other"] return ["movies", "tvshows", "boxsets", "other"]
} }
} }
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -59,13 +59,13 @@ struct LibraryListView: View {
return self.supportedCollectionTypes.contains(collectionType) return self.supportedCollectionTypes.contains(collectionType)
}, id: \.id) { library in }, id: \.id) { library in
Button { Button {
if library.collectionType == "livetv" { if library.collectionType == "livetv" {
libraryListRouter.route(to: \.liveTV) libraryListRouter.route(to: \.liveTV)
} else { } else {
libraryListRouter.route(to: \.library, libraryListRouter.route(to: \.library,
(viewModel: LibraryViewModel(parentID: library.id), (viewModel: LibraryViewModel(parentID: library.id),
title: library.name ?? "")) title: library.name ?? ""))
} }
} label: { } label: {
ZStack { ZStack {
ImageView(library.getPrimaryImage(maxWidth: 500), blurHash: library.getPrimaryImageBlurHash()) ImageView(library.getPrimaryImage(maxWidth: 500), blurHash: library.getPrimaryImageBlurHash())

View File

@ -10,113 +10,113 @@ import JellyfinAPI
import SwiftUI import SwiftUI
struct LiveTVChannelItemElement: View { struct LiveTVChannelItemElement: View {
@FocusState @FocusState
private var focused: Bool private var focused: Bool
@State @State
private var loading: Bool = false private var loading: Bool = false
@State @State
private var isFocused: Bool = false private var isFocused: Bool = false
var channel: BaseItemDto var channel: BaseItemDto
var program: BaseItemDto? var program: BaseItemDto?
var startString = " " var startString = " "
var endString = " " var endString = " "
var progressPercent = Double(0) var progressPercent = Double(0)
var onSelect: (@escaping (Bool) -> Void) -> Void var onSelect: (@escaping (Bool) -> Void) -> Void
private var detailText: String { private var detailText: String {
guard let program = program else { guard let program = program else {
return "" return ""
} }
var text = "" var text = ""
if let season = program.parentIndexNumber, if let season = program.parentIndexNumber,
let episode = program.indexNumber let episode = program.indexNumber
{ {
text.append("\(season)x\(episode) ") text.append("\(season)x\(episode) ")
} else if let episode = program.indexNumber { } else if let episode = program.indexNumber {
text.append("\(episode) ") text.append("\(episode) ")
} }
if let title = program.episodeTitle { if let title = program.episodeTitle {
text.append("\(title) ") text.append("\(title) ")
} }
if let year = program.productionYear { if let year = program.productionYear {
text.append("\(year) ") text.append("\(year) ")
} }
if let rating = program.officialRating { if let rating = program.officialRating {
text.append("\(rating)") text.append("\(rating)")
} }
return text return text
} }
var body: some View { var body: some View {
ZStack { ZStack {
VStack { VStack {
HStack { HStack {
Text(channel.number ?? "") Text(channel.number ?? "")
.font(.footnote) .font(.footnote)
.frame(alignment: .leading) .frame(alignment: .leading)
.padding() .padding()
Spacer() Spacer()
}.frame(alignment: .top) }.frame(alignment: .top)
Spacer() Spacer()
} }
VStack { VStack {
ImageView(channel.getPrimaryImage(maxWidth: 128)) ImageView(channel.getPrimaryImage(maxWidth: 128))
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 128, alignment: .center) .frame(width: 128, alignment: .center)
.padding(.init(top: 8, leading: 0, bottom: 0, trailing: 0)) .padding(.init(top: 8, leading: 0, bottom: 0, trailing: 0))
Text(channel.name ?? "?") Text(channel.name ?? "?")
.font(.footnote) .font(.footnote)
.lineLimit(1) .lineLimit(1)
.frame(alignment: .center) .frame(alignment: .center)
Text(program?.name ?? L10n.notAvailableSlash) Text(program?.name ?? L10n.notAvailableSlash)
.font(.body) .font(.body)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.green) .foregroundColor(.green)
Text(detailText) Text(detailText)
.font(.body) .font(.body)
.lineLimit(1) .lineLimit(1)
.foregroundColor(.green) .foregroundColor(.green)
Spacer() Spacer()
HStack(alignment: .bottom) { HStack(alignment: .bottom) {
VStack { VStack {
Spacer() Spacer()
HStack { HStack {
Text(startString) Text(startString)
.font(.footnote) .font(.footnote)
.lineLimit(1) .lineLimit(1)
.frame(alignment: .leading) .frame(alignment: .leading)
Spacer() Spacer()
Text(endString) Text(endString)
.font(.footnote) .font(.footnote)
.lineLimit(1) .lineLimit(1)
.frame(alignment: .trailing) .frame(alignment: .trailing)
} }
GeometryReader { gp in GeometryReader { gp in
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.fill(Color.gray) .fill(Color.gray)
.opacity(0.4) .opacity(0.4)
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12) .frame(minWidth: 100, maxWidth: .infinity, minHeight: 12, maxHeight: 12)
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.fill(Color.jellyfinPurple) .fill(Color.jellyfinPurple)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 12) .frame(width: CGFloat(progressPercent * gp.size.width), height: 12)
} }
.frame(alignment: .bottom) .frame(alignment: .bottom)
} }
} }
} }
} }
.padding() .padding()
.opacity(loading ? 0.5 : 1.0) .opacity(loading ? 0.5 : 1.0)
if loading { if loading {
ProgressView() ProgressView()
} }
} }
.overlay(RoundedRectangle(cornerRadius: 0) .overlay(RoundedRectangle(cornerRadius: 0)
.stroke(Color.blue, lineWidth: 0)) .stroke(Color.blue, lineWidth: 0))
} }
} }

View File

@ -10,143 +10,142 @@ import JellyfinAPI
import SwiftUI import SwiftUI
struct LiveTVChannelItemWideElement: View { struct LiveTVChannelItemWideElement: View {
@FocusState @FocusState
private var focused: Bool private var focused: Bool
@State @State
private var loading: Bool = false private var loading: Bool = false
@State @State
private var isFocused: Bool = false private var isFocused: Bool = false
var channel: BaseItemDto var channel: BaseItemDto
var currentProgram: BaseItemDto? var currentProgram: BaseItemDto?
var currentProgramText: LiveTVChannelViewProgram var currentProgramText: LiveTVChannelViewProgram
var nextProgramsText: [LiveTVChannelViewProgram] var nextProgramsText: [LiveTVChannelViewProgram]
var onSelect: (@escaping (Bool) -> Void) -> Void var onSelect: (@escaping (Bool) -> Void) -> Void
var progressPercent: Double { var progressPercent: Double {
if let currentProgram = currentProgram { if let currentProgram = currentProgram {
let progressPercent = currentProgram.getLiveProgressPercentage() let progressPercent = currentProgram.getLiveProgressPercentage()
if progressPercent > 1.0 { if progressPercent > 1.0 {
return 1.0 return 1.0
} else { } else {
return progressPercent return progressPercent
} }
} }
return 0 return 0
} }
private var detailText: String {
private var detailText: String { guard let program = currentProgram else {
guard let program = currentProgram else { return ""
return "" }
} var text = ""
var text = "" if let season = program.parentIndexNumber,
if let season = program.parentIndexNumber, let episode = program.indexNumber
let episode = program.indexNumber {
{ text.append("\(season)x\(episode) ")
text.append("\(season)x\(episode) ") } else if let episode = program.indexNumber {
} else if let episode = program.indexNumber { text.append("\(episode) ")
text.append("\(episode) ") }
} if let title = program.episodeTitle {
if let title = program.episodeTitle { text.append("\(title) ")
text.append("\(title) ") }
} if let year = program.productionYear {
if let year = program.productionYear { text.append("\(year) ")
text.append("\(year) ") }
} if let rating = program.officialRating {
if let rating = program.officialRating { text.append("\(rating)")
text.append("\(rating)") }
} return text
return text }
}
var body: some View {
var body: some View { ZStack {
ZStack { ZStack {
ZStack { HStack {
HStack { ZStack(alignment: .center) {
ZStack(alignment: .center) { ImageView(channel.getPrimaryImage(maxWidth: 128))
ImageView(channel.getPrimaryImage(maxWidth: 128)) .aspectRatio(contentMode: .fit)
.aspectRatio(contentMode: .fit) .padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0)) VStack(alignment: .center) {
VStack(alignment: .center) { Spacer()
Spacer() .frame(maxHeight: .infinity)
.frame(maxHeight: .infinity) GeometryReader { gp in
GeometryReader { gp in ZStack(alignment: .leading) {
ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3)
RoundedRectangle(cornerRadius: 3) .fill(Color.gray)
.fill(Color.gray) .opacity(0.4)
.opacity(0.4) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 6, maxHeight: 6)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 6, maxHeight: 6) RoundedRectangle(cornerRadius: 6)
RoundedRectangle(cornerRadius: 6) .fill(Color.jellyfinPurple)
.fill(Color.jellyfinPurple) .frame(width: CGFloat(progressPercent * gp.size.width), height: 6)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 6) }
} }
} .frame(height: 6, alignment: .center)
.frame(height: 6, alignment: .center) .padding(.init(top: 0, leading: 4, bottom: 0, trailing: 4))
.padding(.init(top: 0, leading: 4, bottom: 0, trailing: 4)) }
} if loading {
if loading {
ProgressView()
ProgressView() }
}
} .aspectRatio(1.0, contentMode: .fit)
} VStack(alignment: .leading) {
.aspectRatio(1.0, contentMode: .fit) let channelNumber = channel.number != nil ? "\(channel.number ?? "") " : ""
VStack(alignment: .leading) { let channelName = "\(channelNumber)\(channel.name ?? "?")"
let channelNumber = channel.number != nil ? "\(channel.number ?? "") " : "" Text(channelName)
let channelName = "\(channelNumber)\(channel.name ?? "?")" .font(.body)
Text(channelName) .lineLimit(1)
.font(.body) .foregroundColor(Color.jellyfinPurple)
.lineLimit(1) .frame(alignment: .leading)
.foregroundColor(Color.jellyfinPurple) .padding(.init(top: 0, leading: 0, bottom: 4, trailing: 0))
.frame(alignment: .leading) programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title,
.padding(.init(top: 0, leading: 0, bottom: 4, trailing: 0)) color: Color("TextHighlightColor"))
programLabel(timeText: currentProgramText.timeDisplay, titleText: currentProgramText.title, color: Color("TextHighlightColor")) if !nextProgramsText.isEmpty,
if nextProgramsText.count > 0, let nextItem = nextProgramsText[0]
let nextItem = nextProgramsText[0] { {
programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray) programLabel(timeText: nextItem.timeDisplay, titleText: nextItem.title, color: Color.gray)
} }
if nextProgramsText.count > 1, if nextProgramsText.count > 1,
let nextItem2 = nextProgramsText[1] { let nextItem2 = nextProgramsText[1]
programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray) {
} programLabel(timeText: nextItem2.timeDisplay, titleText: nextItem2.title, color: Color.gray)
Spacer() }
} Spacer()
Spacer() }
} Spacer()
.frame(alignment: .leading) }
.padding() .frame(alignment: .leading)
.opacity(loading ? 0.5 : 1.0) .padding()
} .opacity(loading ? 0.5 : 1.0)
.background( }
RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color("BackgroundSecondaryColor")) .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color("BackgroundSecondaryColor")))
) .frame(height: 128)
.frame(height: 128) .onTapGesture {
.onTapGesture { onSelect { loadingState in
onSelect { loadingState in loading = loadingState
loading = loadingState }
} }
} }
} .background {
.background{ RoundedRectangle(cornerRadius: 10, style: .continuous)
RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(Color("BackgroundColor"))
.fill(Color("BackgroundColor")) .shadow(color: Color("ShadowColor"), radius: 4, x: 0, y: 0)
.shadow(color: Color("ShadowColor"), radius: 4, x: 0, y: 0) }
} }
}
@ViewBuilder
@ViewBuilder func programLabel(timeText: String, titleText: String, color: Color) -> some View {
func programLabel(timeText: String, titleText: String, color: Color) -> some View { HStack(alignment: .top) {
HStack(alignment: .top) { Text(timeText)
Text(timeText) .font(.footnote)
.font(.footnote) .lineLimit(2)
.lineLimit(2) .foregroundColor(color)
.foregroundColor(color) .frame(width: 38, alignment: .leading)
.frame(width: 38, alignment: .leading) Text(titleText)
Text(titleText) .font(.footnote)
.font(.footnote) .lineLimit(2)
.lineLimit(2) .foregroundColor(color)
.foregroundColor(color) }
} }
}
} }

View File

@ -14,204 +14,179 @@ import SwiftUICollection
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String) typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
struct LiveTVChannelsView: View { struct LiveTVChannelsView: View {
@EnvironmentObject @EnvironmentObject
var router: LiveTVCoordinator.Router var router: LiveTVCoordinator.Router
@StateObject @StateObject
var viewModel = LiveTVChannelsViewModel() var viewModel = LiveTVChannelsViewModel()
@State private var isPortrait = false @State
private var isPortrait = false
var body: some View {
if viewModel.isLoading == true {
ProgressView()
} else if !viewModel.rows.isEmpty {
CollectionView(rows: viewModel.rows) { _, _ in
createGridLayout()
} cell: { indexPath, cell in
makeCellView(indexPath: indexPath, cell: cell)
} supplementaryView: { _, indexPath in
EmptyView()
.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
.onAppear {
viewModel.startScheduleCheckTimer()
self.checkOrientation()
}
.onDisappear {
viewModel.stopScheduleCheckTimer()
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
self.checkOrientation()
}
} else {
VStack {
Text("No results.")
Button {
viewModel.getChannels()
} label: {
Text("Reload")
}
}
}
}
@ViewBuilder var body: some View {
func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View { if viewModel.isLoading == true {
let item = cell.item ProgressView()
let channel = item.channel } else if !viewModel.rows.isEmpty {
let currentProgramDisplayText = item.currentProgram?.programDisplayText(timeFormatter: viewModel.timeFormatter) ?? LiveTVChannelViewProgram(timeDisplay: "", title: "") CollectionView(rows: viewModel.rows) { _, _ in
let nextItems = item.programs.filter { program in createGridLayout()
guard let start = program.startDate else { } cell: { indexPath, cell in
return false makeCellView(indexPath: indexPath, cell: cell)
} } supplementaryView: { _, indexPath in
guard let currentStart = item.currentProgram?.startDate else { EmptyView()
return false .accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
} }
return start > currentStart .frame(maxWidth: .infinity, maxHeight: .infinity)
} .ignoresSafeArea()
LiveTVChannelItemWideElement(channel: channel, .onAppear {
currentProgram: item.currentProgram, viewModel.startScheduleCheckTimer()
currentProgramText: currentProgramDisplayText, self.checkOrientation()
nextProgramsText: nextProgramsDisplayText(nextItems: nextItems, timeFormatter: viewModel.timeFormatter), }
onSelect: { loadingAction in .onDisappear {
loadingAction(true) viewModel.stopScheduleCheckTimer()
self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in }
self.router.route(to: \.videoPlayer, playerViewModel) .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.checkOrientation()
loadingAction(false) }
} } else {
} VStack {
}) Text("No results.")
} Button {
viewModel.getChannels()
private func createGridLayout() -> NSCollectionLayoutSection { } label: {
if UIDevice.current.userInterfaceIdiom == .pad { Text("Reload")
let itemSize = NSCollectionLayoutSize( }
widthDimension: .absolute((UIScreen.main.bounds.width / 2) - 16), }
heightDimension: .fractionalHeight(1) }
) }
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(
leading: .flexible(0), top: nil,
trailing: .flexible(2), bottom: .flexible(2)
)
let item2 = NSCollectionLayoutItem(layoutSize: itemSize)
item2.edgeSpacing = NSCollectionLayoutEdgeSpacing(
leading: nil, top: nil,
trailing: .flexible(0), bottom: .flexible(2)
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(144)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item, item2]
)
let section = NSCollectionLayoutSection(group: group)
return section
} else {
if isPortrait {
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute(UIScreen.main.bounds.width - 32),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(
leading: .flexible(0), top: nil,
trailing: .flexible(2), bottom: .flexible(2)
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(144)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
return section
} else {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
var width = (UIScreen.main.bounds.width / 2) - 32
if let safeArea = windowScene?.keyWindow?.safeAreaInsets {
width = (UIScreen.main.bounds.width / 2) - safeArea.left - safeArea.right
}
let itemSize = NSCollectionLayoutSize( @ViewBuilder
widthDimension: .absolute(width), func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
heightDimension: .fractionalHeight(1) let item = cell.item
) let channel = item.channel
let item = NSCollectionLayoutItem(layoutSize: itemSize) let currentProgramDisplayText = item.currentProgram?
item.edgeSpacing = NSCollectionLayoutEdgeSpacing( .programDisplayText(timeFormatter: viewModel.timeFormatter) ?? LiveTVChannelViewProgram(timeDisplay: "", title: "")
leading: .flexible(0), top: nil, let nextItems = item.programs.filter { program in
trailing: .flexible(2), bottom: .flexible(2) guard let start = program.startDate else {
) return false
let item2 = NSCollectionLayoutItem(layoutSize: itemSize) }
item2.edgeSpacing = NSCollectionLayoutEdgeSpacing( guard let currentStart = item.currentProgram?.startDate else {
leading: nil, top: nil, return false
trailing: .flexible(0), bottom: .flexible(2) }
) return start > currentStart
let groupSize = NSCollectionLayoutSize( }
widthDimension: .fractionalWidth(1.0), LiveTVChannelItemWideElement(channel: channel,
heightDimension: .absolute(144) currentProgram: item.currentProgram,
) currentProgramText: currentProgramDisplayText,
let group = NSCollectionLayoutGroup.horizontal( nextProgramsText: nextProgramsDisplayText(nextItems: nextItems,
layoutSize: groupSize, timeFormatter: viewModel.timeFormatter),
subitems: [item, item2] onSelect: { loadingAction in
) loadingAction(true)
let section = NSCollectionLayoutSection(group: group) self.viewModel.fetchVideoPlayerViewModel(item: channel) { playerViewModel in
return section self.router.route(to: \.videoPlayer, playerViewModel)
} DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
} loadingAction(false)
} }
}
private func checkOrientation() { })
let scenes = UIApplication.shared.connectedScenes }
let windowScene = scenes.first as? UIWindowScene
guard let scene = windowScene else { return } private func createGridLayout() -> NSCollectionLayoutSection {
self.isPortrait = scene.interfaceOrientation.isPortrait if UIDevice.current.userInterfaceIdiom == .pad {
} let itemSize = NSCollectionLayoutSize(widthDimension: .absolute((UIScreen.main.bounds.width / 2) - 16),
heightDimension: .fractionalHeight(1))
private func nextProgramsDisplayText(nextItems: [BaseItemDto], timeFormatter: DateFormatter) -> [LiveTVChannelViewProgram] { let item = NSCollectionLayoutItem(layoutSize: itemSize)
var programsDisplayText: [LiveTVChannelViewProgram] = [] item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil,
for item in nextItems { trailing: .flexible(2), bottom: .flexible(2))
programsDisplayText.append(item.programDisplayText(timeFormatter: timeFormatter)) let item2 = NSCollectionLayoutItem(layoutSize: itemSize)
} item2.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil,
return programsDisplayText trailing: .flexible(0), bottom: .flexible(2))
} let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(144))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item, item2])
let section = NSCollectionLayoutSection(group: group)
return section
} else {
if isPortrait {
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(UIScreen.main.bounds.width - 32),
heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil,
trailing: .flexible(2), bottom: .flexible(2))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(144))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
return section
} else {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
var width = (UIScreen.main.bounds.width / 2) - 32
if let safeArea = windowScene?.keyWindow?.safeAreaInsets {
width = (UIScreen.main.bounds.width / 2) - safeArea.left - safeArea.right
}
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(width),
heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil,
trailing: .flexible(2), bottom: .flexible(2))
let item2 = NSCollectionLayoutItem(layoutSize: itemSize)
item2.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil,
trailing: .flexible(0), bottom: .flexible(2))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(144))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item, item2])
let section = NSCollectionLayoutSection(group: group)
return section
}
}
}
private func checkOrientation() {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
guard let scene = windowScene else { return }
self.isPortrait = scene.interfaceOrientation.isPortrait
}
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 { private extension BaseItemDto {
func programDisplayText(timeFormatter: DateFormatter) -> LiveTVChannelViewProgram { func programDisplayText(timeFormatter: DateFormatter) -> LiveTVChannelViewProgram {
var timeText = "" var timeText = ""
if let start = self.startDate { if let start = self.startDate {
timeText.append(timeFormatter.string(from: start) + " ") timeText.append(timeFormatter.string(from: start) + " ")
} }
var displayText = "" var displayText = ""
if let season = self.parentIndexNumber, if let season = self.parentIndexNumber,
let episode = self.indexNumber let episode = self.indexNumber
{ {
displayText.append("\(season)x\(episode) ") displayText.append("\(season)x\(episode) ")
} else if let episode = self.indexNumber { } else if let episode = self.indexNumber {
displayText.append("\(episode) ") displayText.append("\(episode) ")
} }
if let name = self.name { if let name = self.name {
displayText.append("\(name) ") displayText.append("\(name) ")
} }
if let title = self.episodeTitle { if let title = self.episodeTitle {
displayText.append("\(title) ") displayText.append("\(title) ")
} }
if let year = self.productionYear { if let year = self.productionYear {
displayText.append("\(year) ") displayText.append("\(year) ")
} }
if let rating = self.officialRating { if let rating = self.officialRating {
displayText.append("\(rating)") displayText.append("\(rating)")
} }
return LiveTVChannelViewProgram(timeDisplay: timeText, title: displayText) return LiveTVChannelViewProgram(timeDisplay: timeText, title: displayText)
} }
} }

View File

@ -10,201 +10,201 @@ import Stinsen
import SwiftUI import SwiftUI
struct LiveTVProgramsView: View { struct LiveTVProgramsView: View {
@EnvironmentObject @EnvironmentObject
var programsRouter: LiveTVProgramsCoordinator.Router var programsRouter: LiveTVProgramsCoordinator.Router
@StateObject @StateObject
var viewModel = LiveTVProgramsViewModel() var viewModel = LiveTVProgramsViewModel()
var body: some View { var body: some View {
ScrollView { ScrollView {
LazyVStack(alignment: .leading) { LazyVStack(alignment: .leading) {
if !viewModel.recommendedItems.isEmpty, if !viewModel.recommendedItems.isEmpty,
let items = viewModel.recommendedItems let items = viewModel.recommendedItems
{ {
Text("On Now") Text("On Now")
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
.padding(.leading, 90) .padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 45) Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
Button { Button {
if let chanId = item.channelId, if let chanId = item.channelId,
let chan = viewModel.findChannel(id: chanId) let chan = viewModel.findChannel(id: chanId)
{ {
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
self.programsRouter.route(to: \.videoPlayer, playerViewModel) self.programsRouter.route(to: \.videoPlayer, playerViewModel)
} }
} }
} label: { } label: {
#if os(iOS) #if os(iOS)
#elseif os(tvOS) #elseif os(tvOS)
LandscapeItemElement(item: item) LandscapeItemElement(item: item)
#endif #endif
} }
.buttonStyle(PlainNavigationLinkButtonStyle()) .buttonStyle(PlainNavigationLinkButtonStyle())
} }
Spacer().frame(width: 45) Spacer().frame(width: 45)
} }
}.frame(height: 350) }.frame(height: 350)
} }
if !viewModel.seriesItems.isEmpty, if !viewModel.seriesItems.isEmpty,
let items = viewModel.seriesItems let items = viewModel.seriesItems
{ {
Text("Shows") Text("Shows")
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
.padding(.leading, 90) .padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 45) Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
Button { Button {
if let chanId = item.channelId, if let chanId = item.channelId,
let chan = viewModel.findChannel(id: chanId) let chan = viewModel.findChannel(id: chanId)
{ {
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
self.programsRouter.route(to: \.videoPlayer, playerViewModel) self.programsRouter.route(to: \.videoPlayer, playerViewModel)
} }
} }
} label: { } label: {
#if os(iOS) #if os(iOS)
#elseif os(tvOS) #elseif os(tvOS)
LandscapeItemElement(item: item) LandscapeItemElement(item: item)
#endif #endif
} }
.buttonStyle(PlainNavigationLinkButtonStyle()) .buttonStyle(PlainNavigationLinkButtonStyle())
} }
Spacer().frame(width: 45) Spacer().frame(width: 45)
} }
}.frame(height: 350) }.frame(height: 350)
} }
if !viewModel.movieItems.isEmpty, if !viewModel.movieItems.isEmpty,
let items = viewModel.movieItems let items = viewModel.movieItems
{ {
Text("Movies") Text("Movies")
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
.padding(.leading, 90) .padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 45) Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
Button { Button {
if let chanId = item.channelId, if let chanId = item.channelId,
let chan = viewModel.findChannel(id: chanId) let chan = viewModel.findChannel(id: chanId)
{ {
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
self.programsRouter.route(to: \.videoPlayer, playerViewModel) self.programsRouter.route(to: \.videoPlayer, playerViewModel)
} }
} }
} label: { } label: {
#if os(iOS) #if os(iOS)
#elseif os(tvOS) #elseif os(tvOS)
LandscapeItemElement(item: item) LandscapeItemElement(item: item)
#endif #endif
} }
.buttonStyle(PlainNavigationLinkButtonStyle()) .buttonStyle(PlainNavigationLinkButtonStyle())
} }
Spacer().frame(width: 45) Spacer().frame(width: 45)
} }
}.frame(height: 350) }.frame(height: 350)
} }
if !viewModel.sportsItems.isEmpty, if !viewModel.sportsItems.isEmpty,
let items = viewModel.sportsItems let items = viewModel.sportsItems
{ {
Text("Sports") Text("Sports")
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
.padding(.leading, 90) .padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 45) Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
Button { Button {
if let chanId = item.channelId, if let chanId = item.channelId,
let chan = viewModel.findChannel(id: chanId) let chan = viewModel.findChannel(id: chanId)
{ {
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
self.programsRouter.route(to: \.videoPlayer, playerViewModel) self.programsRouter.route(to: \.videoPlayer, playerViewModel)
} }
} }
} label: { } label: {
#if os(iOS) #if os(iOS)
#elseif os(tvOS) #elseif os(tvOS)
LandscapeItemElement(item: item) LandscapeItemElement(item: item)
#endif #endif
} }
.buttonStyle(PlainNavigationLinkButtonStyle()) .buttonStyle(PlainNavigationLinkButtonStyle())
} }
Spacer().frame(width: 45) Spacer().frame(width: 45)
} }
}.frame(height: 350) }.frame(height: 350)
} }
if !viewModel.kidsItems.isEmpty, if !viewModel.kidsItems.isEmpty,
let items = viewModel.kidsItems let items = viewModel.kidsItems
{ {
Text("Kids") Text("Kids")
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
.padding(.leading, 90) .padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 45) Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
Button { Button {
if let chanId = item.channelId, if let chanId = item.channelId,
let chan = viewModel.findChannel(id: chanId) let chan = viewModel.findChannel(id: chanId)
{ {
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
self.programsRouter.route(to: \.videoPlayer, playerViewModel) self.programsRouter.route(to: \.videoPlayer, playerViewModel)
} }
} }
} label: { } label: {
#if os(iOS) #if os(iOS)
#elseif os(tvOS) #elseif os(tvOS)
LandscapeItemElement(item: item) LandscapeItemElement(item: item)
#endif #endif
} }
.buttonStyle(PlainNavigationLinkButtonStyle()) .buttonStyle(PlainNavigationLinkButtonStyle())
} }
Spacer().frame(width: 45) Spacer().frame(width: 45)
} }
}.frame(height: 350) }.frame(height: 350)
} }
if !viewModel.newsItems.isEmpty, if !viewModel.newsItems.isEmpty,
let items = viewModel.newsItems let items = viewModel.newsItems
{ {
Text("News") Text("News")
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
.padding(.leading, 90) .padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { LazyHStack {
Spacer().frame(width: 45) Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in ForEach(items, id: \.id) { item in
Button { Button {
if let chanId = item.channelId, if let chanId = item.channelId,
let chan = viewModel.findChannel(id: chanId) let chan = viewModel.findChannel(id: chanId)
{ {
self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in self.viewModel.fetchVideoPlayerViewModel(item: chan) { playerViewModel in
self.programsRouter.route(to: \.videoPlayer, playerViewModel) self.programsRouter.route(to: \.videoPlayer, playerViewModel)
} }
} }
} label: { } label: {
#if os(iOS) #if os(iOS)
#elseif os(tvOS) #elseif os(tvOS)
LandscapeItemElement(item: item) LandscapeItemElement(item: item)
#endif #endif
} }
.buttonStyle(PlainNavigationLinkButtonStyle()) .buttonStyle(PlainNavigationLinkButtonStyle())
} }
Spacer().frame(width: 45) Spacer().frame(width: 45)
} }
}.frame(height: 350) }.frame(height: 350)
} }
} }
} }
} }
} }

View File

@ -17,13 +17,13 @@ struct ExperimentalSettingsView: View {
var syncSubtitleStateWithAdjacent var syncSubtitleStateWithAdjacent
@Default(.Experimental.nativePlayer) @Default(.Experimental.nativePlayer)
var nativePlayer var nativePlayer
@Default(.Experimental.liveTVAlphaEnabled) @Default(.Experimental.liveTVAlphaEnabled)
var liveTVAlphaEnabled var liveTVAlphaEnabled
@Default(.Experimental.liveTVForceDirectPlay) @Default(.Experimental.liveTVForceDirectPlay)
var liveTVForceDirectPlay var liveTVForceDirectPlay
@Default(.Experimental.liveTVNativePlayer) @Default(.Experimental.liveTVNativePlayer)
var liveTVNativePlayer var liveTVNativePlayer
var body: some View { var body: some View {
Form { Form {
Section { Section {
@ -37,18 +37,18 @@ struct ExperimentalSettingsView: View {
} header: { } header: {
L10n.experimental.text L10n.experimental.text
} }
Section { Section {
Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled) Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled)
Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay) Toggle("Live TV Force Direct Play", isOn: $liveTVForceDirectPlay)
Toggle("Live TV Native Player", isOn: $liveTVNativePlayer) Toggle("Live TV Native Player", isOn: $liveTVNativePlayer)
} header: { } header: {
Text("Live TV") Text("Live TV")
} }
} }
} }
} }

View File

@ -11,28 +11,28 @@ import UIKit
struct LiveTVNativePlayerView: UIViewControllerRepresentable { struct LiveTVNativePlayerView: UIViewControllerRepresentable {
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel
typealias UIViewControllerType = LiveTVNativePlayerViewController typealias UIViewControllerType = LiveTVNativePlayerViewController
func makeUIViewController(context: Context) -> LiveTVNativePlayerViewController { func makeUIViewController(context: Context) -> LiveTVNativePlayerViewController {
LiveTVNativePlayerViewController(viewModel: viewModel) LiveTVNativePlayerViewController(viewModel: viewModel)
} }
func updateUIViewController(_ uiViewController: LiveTVNativePlayerViewController, context: Context) {} func updateUIViewController(_ uiViewController: LiveTVNativePlayerViewController, context: Context) {}
} }
struct LiveTVPlayerView: UIViewControllerRepresentable { struct LiveTVPlayerView: UIViewControllerRepresentable {
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel
typealias UIViewControllerType = LiveTVPlayerViewController typealias UIViewControllerType = LiveTVPlayerViewController
func makeUIViewController(context: Context) -> LiveTVPlayerViewController { func makeUIViewController(context: Context) -> LiveTVPlayerViewController {
LiveTVPlayerViewController(viewModel: viewModel) LiveTVPlayerViewController(viewModel: viewModel)
} }
func updateUIViewController(_ uiViewController: LiveTVPlayerViewController, context: Context) {} func updateUIViewController(_ uiViewController: LiveTVPlayerViewController, context: Context) {}
} }

File diff suppressed because it is too large Load Diff