live tv channels layout ui

This commit is contained in:
jhays 2022-04-24 19:19:15 -05:00
parent 4dac5dd0b9
commit 081857262c
7 changed files with 559 additions and 10 deletions

View File

@ -20,7 +20,8 @@ struct LiveTVChannelRowCell: Hashable {
struct LiveTVChannelProgram: Hashable {
let id = UUID()
let channel: BaseItemDto
let program: BaseItemDto?
let currentProgram: BaseItemDto?
let programs: [BaseItemDto]
}
final class LiveTVChannelsViewModel: ViewModel {
@ -151,7 +152,7 @@ final class LiveTVChannelsViewModel: ViewModel {
}
}
channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg))
channelPrograms.append(LiveTVChannelProgram(channel: channel, currentProgram: currentPrg, programs: prgs))
}
return channelPrograms
}

View File

@ -120,9 +120,7 @@ struct LiveTVChannelItemElement: View {
.stroke(isFocused ? Color.blue : Color.clear, lineWidth: 4))
.cornerRadius(20)
.scaleEffect(isFocused ? 1.1 : 1)
#if os(tvOS)
.focusable(true)
#endif
.focused($focused)
.onChange(of: focused) { foc in
withAnimation(.linear(duration: 0.15)) {

View File

@ -252,9 +252,13 @@
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; };
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; };
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */; };
C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */; };
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; };
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; };
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
C4464953281616AE00DDB461 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
C453497F279A2DA50045F1E2 /* LiveTVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C453497E279A2DA50045F1E2 /* LiveTVPlayerViewController.swift */; };
C4534981279A3F140045F1E2 /* tvOSLiveTVOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */; };
C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */; };
@ -264,8 +268,6 @@
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942C827F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift */; };
C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942CA27F6984100C54FE7 /* LiveTVPlayerViewController.swift */; };
C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942CC27F6994A00C54FE7 /* LiveTVPlayerView.swift */; };
C45942CE27F69BF300C54FE7 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
C45942CF27F69BF500C54FE7 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; };
@ -288,7 +290,7 @@
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; };
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */; };
E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */; };
E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; };
E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; };
@ -740,6 +742,8 @@
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = "<group>"; };
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemWideElement.swift; sourceTree = "<group>"; };
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = "<group>"; };
C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = "<group>"; };
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = "<group>"; };
@ -769,6 +773,7 @@
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = "<group>"; };
E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfoExtensions.swift; sourceTree = "<group>"; };
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
@ -1636,6 +1641,9 @@
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
6213388F265F83A900A81A2A /* LibraryListView.swift */,
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */,
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */,
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */,
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
@ -2077,6 +2085,7 @@
53913C0526D323FE00EB3286 /* Localizable.strings in Resources */,
53913BFF26D323FE00EB3286 /* Localizable.strings in Resources */,
53913C0E26D323FE00EB3286 /* Localizable.strings in Resources */,
C4464953281616AE00DDB461 /* Assets.xcassets in Resources */,
53913BF026D323FE00EB3286 /* Localizable.strings in Resources */,
53913C0826D323FE00EB3286 /* Localizable.strings in Resources */,
53913C1126D323FE00EB3286 /* Localizable.strings in Resources */,
@ -2251,6 +2260,7 @@
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
E193D54B271941D300900D82 /* ServerListView.swift in Sources */,
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */,
62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */,
6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */,
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
@ -2276,7 +2286,6 @@
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */,
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */,
E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
E1A2C15E279A7D9F005EC829 /* AppIcon.swift in Sources */,
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
@ -2387,6 +2396,7 @@
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */,
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */,
@ -2425,6 +2435,7 @@
E1047E2027E584AF00CB0D4A /* BlurHashView.swift in Sources */,
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */,
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */,
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */,
@ -2490,6 +2501,7 @@
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
E1A2C154279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */,
C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */,
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */,
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
@ -2518,12 +2530,10 @@
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
C45942CE27F69BF300C54FE7 /* LiveTVChannelsView.swift in Sources */,
E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */,
E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */,
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
C45942CF27F69BF500C54FE7 /* LiveTVChannelItemElement.swift in Sources */,
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */,
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,

View File

@ -0,0 +1,56 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.100",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.100",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,122 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
struct LiveTVChannelItemElement: View {
@FocusState
private var focused: Bool
@State
private var loading: Bool = false
@State
private var isFocused: Bool = false
var channel: BaseItemDto
var program: BaseItemDto?
var startString = " "
var endString = " "
var progressPercent = Double(0)
var onSelect: (@escaping (Bool) -> Void) -> Void
private var detailText: String {
guard let program = program else {
return ""
}
var text = ""
if let season = program.parentIndexNumber,
let episode = program.indexNumber
{
text.append("\(season)x\(episode) ")
} else if let episode = program.indexNumber {
text.append("\(episode) ")
}
if let title = program.episodeTitle {
text.append("\(title) ")
}
if let year = program.productionYear {
text.append("\(year) ")
}
if let rating = program.officialRating {
text.append("\(rating)")
}
return text
}
var body: some View {
ZStack {
VStack {
HStack {
Text(channel.number ?? "")
.font(.footnote)
.frame(alignment: .leading)
.padding()
Spacer()
}.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()
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)
}
}
}
}
.padding()
.opacity(loading ? 0.5 : 1.0)
if loading {
ProgressView()
}
}
.overlay(RoundedRectangle(cornerRadius: 0)
.stroke(Color.blue, lineWidth: 0))
}
}

View File

@ -0,0 +1,152 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import SwiftUI
struct LiveTVChannelItemWideElement: View {
@FocusState
private var focused: Bool
@State
private var loading: Bool = false
@State
private var isFocused: Bool = false
var channel: BaseItemDto
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 = currentProgram else {
return ""
}
var text = ""
if let season = program.parentIndexNumber,
let episode = program.indexNumber
{
text.append("\(season)x\(episode) ")
} else if let episode = program.indexNumber {
text.append("\(episode) ")
}
if let title = program.episodeTitle {
text.append("\(title) ")
}
if let year = program.productionYear {
text.append("\(year) ")
}
if let rating = program.officialRating {
text.append("\(rating)")
}
return text
}
var body: some View {
ZStack {
HStack {
ZStack(alignment: .bottomLeading) {
ImageView(channel.getPrimaryImage(maxWidth: 128))
.aspectRatio(contentMode: .fit)
.padding(.init(top: 0, leading: 0, bottom: 8, trailing: 0))
VStack {
Spacer()
.frame(maxHeight: .infinity)
GeometryReader { gp in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3)
.fill(Color.gray)
.opacity(0.4)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 6, maxHeight: 6)
RoundedRectangle(cornerRadius: 6)
.fill(Color.jellyfinPurple)
.frame(width: CGFloat(progressPercent * gp.size.width), height: 6)
}
}
.frame(height: 6, alignment: .bottomLeading)
.padding(.init(top: 0, leading: 0, bottom: 0, trailing: 8))
}
}
.aspectRatio(1.0, contentMode: .fit)
VStack(alignment: .leading) {
let channelNumber = channel.number != nil ? "\(channel.number ?? "") " : ""
let channelName = "\(channelNumber)\(channel.name ?? "?")"
Text(channelName)
.font(.body)
.lineLimit(1)
.frame(alignment: .leading)
HStack(alignment: .top) {
Text(currentProgramText.timeDisplay)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.green)
.frame(width: 40)
Text(currentProgramText.title)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.green)
}
if nextProgramsText.count > 0,
let nextItem = nextProgramsText[0] {
HStack(alignment: .top) {
Text(nextItem.timeDisplay)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
.frame(width: 40)
Text(nextItem.title)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
}
}
if nextProgramsText.count > 1,
let nextItem2 = nextProgramsText[1] {
HStack(alignment: .top) {
Text(nextItem2.timeDisplay)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
.frame(width: 40)
Text(nextItem2.title)
.font(.footnote)
.lineLimit(2)
.foregroundColor(.gray)
}
}
Spacer()
}
Spacer()
}
.frame(alignment: .leading)
.padding()
.opacity(loading ? 0.5 : 1.0)
if loading {
ProgressView()
}
}
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous).fill(Color("BackgroundColor"))
)
.frame(height: 128)
}
}

View File

@ -0,0 +1,210 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
import SwiftUI
import SwiftUICollection
typealias LiveTVChannelViewProgram = (timeDisplay: String, title: String)
struct LiveTVChannelsView: View {
@EnvironmentObject
var router: LiveTVChannelsCoordinator.Router
@StateObject
var viewModel = LiveTVChannelsViewModel()
@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
func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View {
let item = cell.item
let channel = item.channel
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
}
LiveTVChannelItemWideElement(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 {
if UIDevice.current.userInterfaceIdiom == .pad {
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute((UIScreen.main.bounds.width / 2) - 2),
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(132)
)
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 - 2),
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(132)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
return section
} else {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.49),
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(132)
)
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
print("orientationDidChange: isPortrait? \(self.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 {
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)
}
}