live tv channels layout ui
This commit is contained in:
parent
4dac5dd0b9
commit
081857262c
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue