From 1fded3ee8ed356507f9fc62ae1e4bf9712f3016e Mon Sep 17 00:00:00 2001 From: jhays Date: Tue, 26 Oct 2021 09:06:11 -0500 Subject: [PATCH 01/11] start LiveTV section --- .../Views/LibraryListView.swift | 12 +- .../Views/LiveTVGuideView.swift | 22 +++ .../Views/LiveTVProgramsView.swift | 149 +++++++++++++++ JellyfinPlayer.xcodeproj/project.pbxproj | 38 ++++ .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Coordinators/LibraryListCoordinator.swift | 13 +- .../Coordinators/LiveTVGuideCoordinator.swift | 30 +++ .../LiveTVProgramsCoordinator.swift | 30 +++ .../Coordinators/LiveTVTabCoordinator.swift | 44 +++++ .../iOSMainTabCoordinator.swift | 2 +- .../tvOSMainTabCoordinator.swift | 2 +- Shared/ViewModels/LiveTVGuideViewModel.swift | 15 ++ .../ViewModels/LiveTVProgramsViewModel.swift | 171 ++++++++++++++++++ 13 files changed, 524 insertions(+), 8 deletions(-) create mode 100644 JellyfinPlayer tvOS/Views/LiveTVGuideView.swift create mode 100644 JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift create mode 100644 Shared/Coordinators/LiveTVGuideCoordinator.swift create mode 100644 Shared/Coordinators/LiveTVProgramsCoordinator.swift create mode 100644 Shared/Coordinators/LiveTVTabCoordinator.swift create mode 100644 Shared/ViewModels/LiveTVGuideViewModel.swift create mode 100644 Shared/ViewModels/LiveTVProgramsViewModel.swift diff --git a/JellyfinPlayer tvOS/Views/LibraryListView.swift b/JellyfinPlayer tvOS/Views/LibraryListView.swift index d7ecd279..e3d4d516 100644 --- a/JellyfinPlayer tvOS/Views/LibraryListView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryListView.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI struct LibraryListView: View { + @EnvironmentObject var libraryListRouter: LibraryListCoordinator.Router @StateObject var viewModel = LibraryListViewModel() var body: some View { @@ -21,9 +22,14 @@ struct LibraryListView: View { if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" || library.collectionType ?? "" == "music" { EmptyView() } else { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") - }) { + Button() { + if library.collectionType == "livetv" { + self.libraryListRouter.route(to: \.liveTvTabs) + } else { + self.libraryListRouter.route(to: \.library, (viewModel: LibraryViewModel(), title: library.name ?? "")) + } + } + label: { ZStack { ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) .opacity(0.4) diff --git a/JellyfinPlayer tvOS/Views/LiveTVGuideView.swift b/JellyfinPlayer tvOS/Views/LiveTVGuideView.swift new file mode 100644 index 00000000..d7d2b56d --- /dev/null +++ b/JellyfinPlayer tvOS/Views/LiveTVGuideView.swift @@ -0,0 +1,22 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +struct LiveTVGuideView: View { + @EnvironmentObject var programsRouter: LiveTVGuideCoordinator.Router + @StateObject var viewModel = LiveTVGuideViewModel() + + var body: some View { + Button {} label: { + Text("Coming Soon") + } + } +} diff --git a/JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift b/JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift new file mode 100644 index 00000000..f979848b --- /dev/null +++ b/JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift @@ -0,0 +1,149 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +struct LiveTVProgramsView: View { + @EnvironmentObject var programsRouter: LiveTVProgramsCoordinator.Router + @StateObject var viewModel = LiveTVProgramsViewModel() + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading) { + if !viewModel.recommendedItems.isEmpty, + let items = viewModel.recommendedItems { + Text("On Now") + .font(.headline) + .fontWeight(.semibold) + .padding(.leading, 90) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(items, id: \.id) { item in + Button { + self.programsRouter.route(to: \.modalItem, item) + } label: { + LandscapeItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.frame(height: 350) + } + if !viewModel.seriesItems.isEmpty, + let items = viewModel.seriesItems { + Text("Shows") + .font(.headline) + .fontWeight(.semibold) + .padding(.leading, 90) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(items, id: \.id) { item in + Button { + self.programsRouter.route(to: \.modalItem, item) + } label: { + LandscapeItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.frame(height: 350) + } + if !viewModel.movieItems.isEmpty, + let items = viewModel.movieItems { + Text("Movies") + .font(.headline) + .fontWeight(.semibold) + .padding(.leading, 90) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(items, id: \.id) { item in + Button { + self.programsRouter.route(to: \.modalItem, item) + } label: { + LandscapeItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.frame(height: 350) + } + if !viewModel.sportsItems.isEmpty, + let items = viewModel.sportsItems { + Text("Sports") + .font(.headline) + .fontWeight(.semibold) + .padding(.leading, 90) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(items, id: \.id) { item in + Button { + self.programsRouter.route(to: \.modalItem, item) + } label: { + LandscapeItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.frame(height: 350) + } + if !viewModel.kidsItems.isEmpty, + let items = viewModel.kidsItems { + Text("Kids") + .font(.headline) + .fontWeight(.semibold) + .padding(.leading, 90) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(items, id: \.id) { item in + Button { + self.programsRouter.route(to: \.modalItem, item) + } label: { + LandscapeItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.frame(height: 350) + } + if !viewModel.newsItems.isEmpty, + let items = viewModel.newsItems { + Text("News") + .font(.headline) + .fontWeight(.semibold) + .padding(.leading, 90) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + Spacer().frame(width: 45) + ForEach(items, id: \.id) { item in + Button { + self.programsRouter.route(to: \.modalItem, item) + } label: { + LandscapeItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + Spacer().frame(width: 45) + } + }.frame(height: 350) + } + } + } + } +} diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 54cc4ac8..199f9b16 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -241,6 +241,18 @@ C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; }; C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */; }; C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; + C4BE07712725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; }; + C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; }; + C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */; }; + C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; }; + C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; }; + C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; }; + C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; }; + C4BE077C272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */; }; + C4BE077D272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */; }; + C4BE0780272837FB003F4AD1 /* LiveTVGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */; }; + C4BE07822728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */; }; + C4BE07832728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */; }; C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; }; C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; @@ -561,6 +573,13 @@ C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = ""; }; C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = ""; }; C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = ""; }; + C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsCoordinator.swift; sourceTree = ""; }; + C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = ""; }; + C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsViewModel.swift; sourceTree = ""; }; + C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVTabCoordinator.swift; sourceTree = ""; }; + C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideViewModel.swift; sourceTree = ""; }; + C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideView.swift; sourceTree = ""; }; + C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideCoordinator.swift; sourceTree = ""; }; C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; @@ -716,7 +735,9 @@ 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, + C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, + C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */, C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */, 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */, @@ -1104,6 +1125,9 @@ E193D5412719404B00900D82 /* MainCoordinator */, 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, + C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */, + C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */, + C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */, C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */, @@ -1179,6 +1203,8 @@ C4E508172703E8190045C9AB /* LibraryListView.swift */, C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, + C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */, + C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */, C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */, 531690EE267ABF72005D8AB9 /* NextUpView.swift */, @@ -1758,6 +1784,7 @@ buildActionMask = 2147483647; files = ( E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */, + C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */, 531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */, E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */, @@ -1778,11 +1805,13 @@ E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */, 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, E193D53E27193F9A00900D82 /* VideoPlayerCoordinator.swift in Sources */, + C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, + C4BE0780272837FB003F4AD1 /* LiveTVGuideView.swift in Sources */, 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, @@ -1791,6 +1820,7 @@ 62671DB327159C1800199D95 /* ItemCoordinator.swift in Sources */, E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */, E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */, + C4BE07832728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */, 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, @@ -1851,6 +1881,7 @@ E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, + C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */, E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, 6264E88D273850380081A12A /* Strings.swift in Sources */, @@ -1870,8 +1901,10 @@ 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, + C4BE077D272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, + C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */, E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */, @@ -1931,6 +1964,7 @@ 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, + C4BE077C272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */, 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, @@ -1946,21 +1980,25 @@ 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, + C4BE07822728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, + C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */, E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */, + C4BE07712725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, + C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */, E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, E13DD3BD27163C63009D4DAF /* EmailHelper.swift in Sources */, E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */, diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5378c461..de6339e3 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -87,8 +87,8 @@ "repositoryURL": "https://github.com/rundfunk47/stinsen", "state": { "branch": null, - "revision": "3d06c7603c70f8af1bd49f8d49f17e98f25b2d6a", - "version": "2.0.2" + "revision": "5e6c714f6f308877c8a988523915f9eb592d7d82", + "version": "2.0.3" } }, { diff --git a/Shared/Coordinators/LibraryListCoordinator.swift b/Shared/Coordinators/LibraryListCoordinator.swift index e79f25c2..f3d8eea2 100644 --- a/Shared/Coordinators/LibraryListCoordinator.swift +++ b/Shared/Coordinators/LibraryListCoordinator.swift @@ -18,7 +18,14 @@ final class LibraryListCoordinator: NavigationCoordinatable { @Root var start = makeStart @Route(.push) var search = makeSearch @Route(.push) var library = makeLibrary + @Route(.modal) var liveTvTabs = makeLiveTvTabs + + let viewModel: LibraryListViewModel + init(viewModel: LibraryListViewModel) { + self.viewModel = viewModel + } + func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { LibraryCoordinator(viewModel: params.viewModel, title: params.title) } @@ -26,9 +33,13 @@ final class LibraryListCoordinator: NavigationCoordinatable { func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { SearchCoordinator(viewModel: viewModel) } + + func makeLiveTvTabs() -> LiveTVTabCoordinator { + LiveTVTabCoordinator() + } @ViewBuilder func makeStart() -> some View { - LibraryListView() + LibraryListView(viewModel: self.viewModel) } } diff --git a/Shared/Coordinators/LiveTVGuideCoordinator.swift b/Shared/Coordinators/LiveTVGuideCoordinator.swift new file mode 100644 index 00000000..7e176f7c --- /dev/null +++ b/Shared/Coordinators/LiveTVGuideCoordinator.swift @@ -0,0 +1,30 @@ +// +/* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +final class LiveTVGuideCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \LiveTVGuideCoordinator.start) + + @Root var start = makeStart + @Route(.modal) var modalItem = makeModalItem + + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { + return NavigationViewCoordinator(ItemCoordinator(item: item)) + } + + @ViewBuilder + func makeStart() -> some View { + LiveTVGuideView() + } +} diff --git a/Shared/Coordinators/LiveTVProgramsCoordinator.swift b/Shared/Coordinators/LiveTVProgramsCoordinator.swift new file mode 100644 index 00000000..c500697e --- /dev/null +++ b/Shared/Coordinators/LiveTVProgramsCoordinator.swift @@ -0,0 +1,30 @@ +// +/* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +final class LiveTVProgramsCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start) + + @Root var start = makeStart + @Route(.modal) var modalItem = makeModalItem + + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { + return NavigationViewCoordinator(ItemCoordinator(item: item)) + } + + @ViewBuilder + func makeStart() -> some View { + LiveTVProgramsView() + } +} diff --git a/Shared/Coordinators/LiveTVTabCoordinator.swift b/Shared/Coordinators/LiveTVTabCoordinator.swift new file mode 100644 index 00000000..9d0f3998 --- /dev/null +++ b/Shared/Coordinators/LiveTVTabCoordinator.swift @@ -0,0 +1,44 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI +import Stinsen + +final class LiveTVTabCoordinator: TabCoordinatable { + var child = TabChild(startingItems: [ + \LiveTVTabCoordinator.programs, + \LiveTVTabCoordinator.guide + ]) + + @Route(tabItem: makeProgramsTab) var programs = makePrograms + @Route(tabItem: makeGuideTab) var guide = makeGuide + + func makePrograms() -> NavigationViewCoordinator { + return NavigationViewCoordinator(LiveTVProgramsCoordinator()) + } + + @ViewBuilder func makeProgramsTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "tv") + Text("Programs") + } + } + + func makeGuide() -> NavigationViewCoordinator { + return NavigationViewCoordinator(LiveTVGuideCoordinator()) + } + + @ViewBuilder func makeGuideTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "calendar") + Text("Guide") + } + } +} diff --git a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift index cfa95e16..d3777a77 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift @@ -30,7 +30,7 @@ final class MainTabCoordinator: TabCoordinatable { } func makeAllMedia() -> NavigationViewCoordinator { - return NavigationViewCoordinator(LibraryListCoordinator()) + return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) } @ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View { diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index ea0a5be7..0b40c1e3 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -60,7 +60,7 @@ final class MainTabCoordinator: TabCoordinatable { } func makeOther() -> NavigationViewCoordinator { - return NavigationViewCoordinator(LibraryListCoordinator()) + return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) } @ViewBuilder func makeOtherTab(isActive: Bool) -> some View { diff --git a/Shared/ViewModels/LiveTVGuideViewModel.swift b/Shared/ViewModels/LiveTVGuideViewModel.swift new file mode 100644 index 00000000..4a2fa5ff --- /dev/null +++ b/Shared/ViewModels/LiveTVGuideViewModel.swift @@ -0,0 +1,15 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +final class LiveTVGuideViewModel: ViewModel { + +} diff --git a/Shared/ViewModels/LiveTVProgramsViewModel.swift b/Shared/ViewModels/LiveTVProgramsViewModel.swift new file mode 100644 index 00000000..29023213 --- /dev/null +++ b/Shared/ViewModels/LiveTVProgramsViewModel.swift @@ -0,0 +1,171 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +final class LiveTVProgramsViewModel: ViewModel { + + @Published var recommendedItems = [BaseItemDto]() + @Published var seriesItems = [BaseItemDto]() + @Published var movieItems = [BaseItemDto]() + @Published var sportsItems = [BaseItemDto]() + @Published var kidsItems = [BaseItemDto]() + @Published var newsItems = [BaseItemDto]() + + override init() { + super.init() + + loadRecommendedPrograms() + loadSeries() + loadMovies() + loadSports() + loadKids() + loadNews() + } + + private func loadRecommendedPrograms() { + LiveTvAPI.getRecommendedPrograms( + userId: SessionManager.main.currentLogin.user.id, + limit: 9, + isAiring: true, + imageTypeLimit: 1, + enableImageTypes: [.primary, .thumb], + fields: [.channelInfo, .primaryImageAspectRatio], + enableTotalRecordCount: false + ) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Recommended Programs") + guard let self = self else { return } + self.recommendedItems = response.items ?? [] + }) + .store(in: &cancellables) + } + + private func loadSeries() { + let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, + hasAired: false, + isMovie: false, + isSeries: true, + isNews: false, + isKids: false, + isSports: false, + limit: 9, + enableTotalRecordCount: false, + enableImageTypes: [.primary, .thumb], + fields: [.channelInfo, .primaryImageAspectRatio] + ) + + LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Series Items") + guard let self = self else { return } + self.seriesItems = response.items ?? [] + }) + .store(in: &cancellables) + } + + private func loadMovies() { + let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, + hasAired: false, + isMovie: true, + isSeries: false, + isNews: false, + isKids: false, + isSports: false, + limit: 9, + enableTotalRecordCount: false, + enableImageTypes: [.primary, .thumb], + fields: [.channelInfo, .primaryImageAspectRatio] + ) + + LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Movie Items") + guard let self = self else { return } + self.movieItems = response.items ?? [] + }) + .store(in: &cancellables) + } + + private func loadSports() { + let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, + hasAired: false, + isSports: true, + limit: 9, + enableTotalRecordCount: false, + enableImageTypes: [.primary, .thumb], + fields: [.channelInfo, .primaryImageAspectRatio] + ) + + LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Sports Items") + guard let self = self else { return } + self.sportsItems = response.items ?? [] + }) + .store(in: &cancellables) + } + + private func loadKids() { + let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, + hasAired: false, + isKids: true, + limit: 9, + enableTotalRecordCount: false, + enableImageTypes: [.primary, .thumb], + fields: [.channelInfo, .primaryImageAspectRatio] + ) + + LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Kids Items") + guard let self = self else { return } + self.kidsItems = response.items ?? [] + }) + .store(in: &cancellables) + } + + private func loadNews() { + let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, + hasAired: false, + isNews: true, + limit: 9, + enableTotalRecordCount: false, + enableImageTypes: [.primary, .thumb], + fields: [.channelInfo, .primaryImageAspectRatio] + ) + + LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) News Items") + guard let self = self else { return } + self.newsItems = response.items ?? [] + }) + .store(in: &cancellables) + } +} From b7fd00702f5850e39f51ac9840434ccd0162e04d Mon Sep 17 00:00:00 2001 From: jhays Date: Tue, 26 Oct 2021 09:20:37 -0500 Subject: [PATCH 02/11] add Channels tab --- .../Views/LiveTVChannelsView.swift | 22 ++++++++++++++ JellyfinPlayer.xcodeproj/project.pbxproj | 18 ++++++++++++ .../LiveTVChannelsCoordinator.swift | 29 +++++++++++++++++++ .../Coordinators/LiveTVTabCoordinator.swift | 17 +++++++++-- .../ViewModels/LiveTVChannelsViewModel.swift | 15 ++++++++++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift create mode 100644 Shared/Coordinators/LiveTVChannelsCoordinator.swift create mode 100644 Shared/ViewModels/LiveTVChannelsViewModel.swift diff --git a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift new file mode 100644 index 00000000..71774eeb --- /dev/null +++ b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift @@ -0,0 +1,22 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +struct LiveTVChannelsView: View { + @EnvironmentObject var programsRouter: LiveTVChannelsCoordinator.Router + @StateObject var viewModel = LiveTVChannelsViewModel() + + var body: some View { + Button {} label: { + Text("Coming Soon") + } + } +} diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 199f9b16..ccbca006 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -253,6 +253,12 @@ C4BE0780272837FB003F4AD1 /* LiveTVGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */; }; C4BE07822728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */; }; C4BE07832728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */; }; + C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; }; + C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; }; + C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; }; + C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; }; + C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; }; + C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; }; C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; }; C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; @@ -580,6 +586,9 @@ C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideViewModel.swift; sourceTree = ""; }; C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideView.swift; sourceTree = ""; }; C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideCoordinator.swift; sourceTree = ""; }; + C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsViewModel.swift; sourceTree = ""; }; + C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsCoordinator.swift; sourceTree = ""; }; + C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; @@ -738,6 +747,7 @@ C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */, + C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */, 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */, @@ -1128,6 +1138,7 @@ C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */, C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */, C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */, + C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */, C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */, @@ -1205,6 +1216,7 @@ 53A83C32268A309300DF3D92 /* LibraryView.swift */, C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */, C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */, + C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */, C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */, 531690EE267ABF72005D8AB9 /* NextUpView.swift */, @@ -1839,9 +1851,11 @@ E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */, E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */, E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */, + C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */, E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, + C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */, E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */, 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, @@ -1850,6 +1864,7 @@ E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, + C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, @@ -1940,7 +1955,9 @@ 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, + C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, + C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, @@ -2019,6 +2036,7 @@ E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, + C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */, diff --git a/Shared/Coordinators/LiveTVChannelsCoordinator.swift b/Shared/Coordinators/LiveTVChannelsCoordinator.swift new file mode 100644 index 00000000..17b683d4 --- /dev/null +++ b/Shared/Coordinators/LiveTVChannelsCoordinator.swift @@ -0,0 +1,29 @@ +// +/* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +final class LiveTVChannelsCoordinator: NavigationCoordinatable { + let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start) + + @Root var start = makeStart + @Route(.modal) var modalItem = makeModalItem + + func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { + return NavigationViewCoordinator(ItemCoordinator(item: item)) + } + + @ViewBuilder + func makeStart() -> some View { + LiveTVChannelsView() + } +} diff --git a/Shared/Coordinators/LiveTVTabCoordinator.swift b/Shared/Coordinators/LiveTVTabCoordinator.swift index 9d0f3998..971c83b5 100644 --- a/Shared/Coordinators/LiveTVTabCoordinator.swift +++ b/Shared/Coordinators/LiveTVTabCoordinator.swift @@ -13,12 +13,14 @@ import Stinsen final class LiveTVTabCoordinator: TabCoordinatable { var child = TabChild(startingItems: [ - \LiveTVTabCoordinator.programs, - \LiveTVTabCoordinator.guide + \LiveTVTabCoordinator.programs, + \LiveTVTabCoordinator.guide, + \LiveTVTabCoordinator.channels ]) @Route(tabItem: makeProgramsTab) var programs = makePrograms @Route(tabItem: makeGuideTab) var guide = makeGuide + @Route(tabItem: makeChannelsTab) var channels = makeChannels func makePrograms() -> NavigationViewCoordinator { return NavigationViewCoordinator(LiveTVProgramsCoordinator()) @@ -41,4 +43,15 @@ final class LiveTVTabCoordinator: TabCoordinatable { Text("Guide") } } + + func makeChannels() -> NavigationViewCoordinator { + return NavigationViewCoordinator(LiveTVChannelsCoordinator()) + } + + @ViewBuilder func makeChannelsTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "square.grid.3x3") + Text("Channels") + } + } } diff --git a/Shared/ViewModels/LiveTVChannelsViewModel.swift b/Shared/ViewModels/LiveTVChannelsViewModel.swift new file mode 100644 index 00000000..ce29d882 --- /dev/null +++ b/Shared/ViewModels/LiveTVChannelsViewModel.swift @@ -0,0 +1,15 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +final class LiveTVChannelsViewModel: ViewModel { + +} From 25ec19b1fe51983e9fdc4eb0a959e14de5237d75 Mon Sep 17 00:00:00 2001 From: jhays Date: Tue, 26 Oct 2021 21:10:17 -0500 Subject: [PATCH 03/11] LiveTV as root --- JellyfinPlayer tvOS/Views/LibraryListView.swift | 3 ++- Shared/Coordinators/LibraryListCoordinator.swift | 5 ----- .../Coordinators/MainCoordinator/tvOSMainCoordinator.swift | 7 ++++++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/JellyfinPlayer tvOS/Views/LibraryListView.swift b/JellyfinPlayer tvOS/Views/LibraryListView.swift index e3d4d516..7c50212f 100644 --- a/JellyfinPlayer tvOS/Views/LibraryListView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryListView.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI struct LibraryListView: View { + @EnvironmentObject var mainCoordinator: MainCoordinator.Router @EnvironmentObject var libraryListRouter: LibraryListCoordinator.Router @StateObject var viewModel = LibraryListViewModel() @@ -24,7 +25,7 @@ struct LibraryListView: View { } else { Button() { if library.collectionType == "livetv" { - self.libraryListRouter.route(to: \.liveTvTabs) + self.mainCoordinator.root(\.liveTV) } else { self.libraryListRouter.route(to: \.library, (viewModel: LibraryViewModel(), title: library.name ?? "")) } diff --git a/Shared/Coordinators/LibraryListCoordinator.swift b/Shared/Coordinators/LibraryListCoordinator.swift index f3d8eea2..932004bf 100644 --- a/Shared/Coordinators/LibraryListCoordinator.swift +++ b/Shared/Coordinators/LibraryListCoordinator.swift @@ -18,7 +18,6 @@ final class LibraryListCoordinator: NavigationCoordinatable { @Root var start = makeStart @Route(.push) var search = makeSearch @Route(.push) var library = makeLibrary - @Route(.modal) var liveTvTabs = makeLiveTvTabs let viewModel: LibraryListViewModel @@ -33,10 +32,6 @@ final class LibraryListCoordinator: NavigationCoordinatable { func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { SearchCoordinator(viewModel: viewModel) } - - func makeLiveTvTabs() -> LiveTVTabCoordinator { - LiveTVTabCoordinator() - } @ViewBuilder func makeStart() -> some View { diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift index 122a870f..a9dfdc96 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift @@ -17,7 +17,8 @@ final class MainCoordinator: NavigationCoordinatable { @Root var mainTab = makeMainTab @Root var serverList = makeServerList - + @Root var liveTV = makeLiveTV + init() { if SessionManager.main.currentLogin != nil { self.stack = NavigationStack(initial: \MainCoordinator.mainTab) @@ -51,4 +52,8 @@ final class MainCoordinator: NavigationCoordinatable { func makeServerList() -> NavigationViewCoordinator { NavigationViewCoordinator(ServerListCoordinator()) } + + func makeLiveTV() -> LiveTVTabCoordinator { + LiveTVTabCoordinator() + } } From 7dab722cda787c44e0909064bb9d93555477cf6e Mon Sep 17 00:00:00 2001 From: jhays Date: Thu, 18 Nov 2021 08:09:53 -0600 Subject: [PATCH 04/11] Add live TV channels grid. Remove guide view for now. --- .../Components/LandscapeItemElement.swift | 2 +- .../Components/LiveTVChannelItemElement.swift | 98 +++++++++ .../Views/LiveTVChannelsView.swift | 80 +++++++- ...TVGuideView.swift => LiveTVHomeView.swift} | 11 +- JellyfinPlayer.xcodeproj/project.pbxproj | 26 +-- .../Coordinators/LiveTVGuideCoordinator.swift | 30 --- .../Coordinators/LiveTVTabCoordinator.swift | 30 +-- .../ViewModels/LiveTVChannelsViewModel.swift | 187 ++++++++++++++++++ Shared/ViewModels/LiveTVGuideViewModel.swift | 15 -- 9 files changed, 394 insertions(+), 85 deletions(-) create mode 100644 JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift rename JellyfinPlayer tvOS/Views/{LiveTVGuideView.swift => LiveTVHomeView.swift} (64%) delete mode 100644 Shared/Coordinators/LiveTVGuideCoordinator.swift delete mode 100644 Shared/ViewModels/LiveTVGuideViewModel.swift diff --git a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift index 23b07c1d..eb6b6a01 100644 --- a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift +++ b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift @@ -10,7 +10,7 @@ import SwiftUI import JellyfinAPI -private struct CutOffShadow: Shape { +struct CutOffShadow: Shape { func path(in rect: CGRect) -> Path { var path = Path() diff --git a/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift b/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift new file mode 100644 index 00000000..0e2037a9 --- /dev/null +++ b/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift @@ -0,0 +1,98 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + + +import SwiftUI +import JellyfinAPI + +struct LiveTVChannelItemElement: View { + @Environment(\.isFocused) var envFocused: Bool + @State var focused: Bool = false + + var channel: BaseItemDto + var program: BaseItemDto? + + var dateFormatter: DateFormatter { + let df = DateFormatter() + df.dateFormat = "h:mm" + return df + } + + var body: some View { + VStack { + HStack { + Spacer() + Text(channel.number ?? "") + .font(.footnote) + .frame(alignment: .trailing) + }.frame(alignment: .top) + ImageView(src: channel.getPrimaryImage(maxWidth: 125)) + .frame(width: 125, alignment: .center) + .offset(x: 0, y: -32) + Text(channel.name ?? "?") + .font(.footnote) + .lineLimit(1) + .frame(alignment: .center) + if let currentProgram = program { + Text(currentProgram.name ?? "") + .font(.body) + .lineLimit(1) + .foregroundColor(.green) + } + + if let currentProgram = program, + let start = currentProgram.startDate?.toLocalTime().timeIntervalSinceReferenceDate, + let end = currentProgram.endDate?.toLocalTime().timeIntervalSinceReferenceDate { + let now = Date().timeIntervalSinceReferenceDate + let length = end - start + let progress = now - start + let progPercent = progress / length + + VStack { + if let startDate = currentProgram.startDate, + let endDate = currentProgram.endDate { + HStack { + Text(dateFormatter.string(from: startDate.toLocalTime())) + .font(.footnote) + .lineLimit(1) + .frame(alignment: .leading) + + Spacer() + + Text(dateFormatter.string(from: endDate.toLocalTime())) + .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(red: 172/255, green: 92/255, blue: 195/255)) + .frame(width: CGFloat(progPercent * gp.size.width), height: 12) + } + } + } + } + } + .padding() + .background(Color.clear) + .border(focused ? Color.blue : Color.clear, width: 4) + .onChange(of: envFocused) { envFocus in + withAnimation(.linear(duration: 0.15)) { + self.focused = envFocus + } + } + .scaleEffect(focused ? 1.1 : 1) + } +} diff --git a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift index 71774eeb..8e2fd05d 100644 --- a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift +++ b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift @@ -9,14 +9,88 @@ import Foundation import SwiftUI +import SwiftUICollection + struct LiveTVChannelsView: View { @EnvironmentObject var programsRouter: LiveTVChannelsCoordinator.Router @StateObject var viewModel = LiveTVChannelsViewModel() - + var body: some View { - Button {} label: { - Text("Coming Soon") + if viewModel.isLoading == true { + ProgressView() + } else if !viewModel.rows.isEmpty { + CollectionView(rows: viewModel.rows) { section, env in + return 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() + + } else { + VStack { + Text("No results.") + Button { + print("movieLibraries reload") + } label: { + Text("Reload") + } + } } } + + @ViewBuilder func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View { + GeometryReader { _ in + if let item = cell.item, + let channel = item.channel{ + if channel.type != "Folder" { + Button { + } label: { + LiveTVChannelItemElement(channel: channel, program: item.program) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + } + } + } + } + + private func createGridLayout() -> NSCollectionLayoutSection { + // I don't know why tvOS has a margin on the sides of a collection view + // But it does, even with contentInset = .zero and ignoreSafeArea. + let sideMargin = CGFloat(30) + let itemWidth = (UIScreen.main.bounds.width / 4) - (sideMargin * 2) + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(itemWidth), + heightDimension: .absolute(itemWidth)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + item.edgeSpacing = .init( + leading: .fixed(8), + top: .fixed(8), + trailing: .fixed(8), + bottom: .fixed(8) + ) + + + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(itemWidth)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, + subitems: [item]) + group.edgeSpacing = .init( + leading: .fixed(0), + top: .fixed(16), + trailing: .fixed(0), + bottom: .fixed(16) + ) + group.contentInsets = .zero + + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = .zero + + return section + } + } diff --git a/JellyfinPlayer tvOS/Views/LiveTVGuideView.swift b/JellyfinPlayer tvOS/Views/LiveTVHomeView.swift similarity index 64% rename from JellyfinPlayer tvOS/Views/LiveTVGuideView.swift rename to JellyfinPlayer tvOS/Views/LiveTVHomeView.swift index d7d2b56d..691dbc27 100644 --- a/JellyfinPlayer tvOS/Views/LiveTVGuideView.swift +++ b/JellyfinPlayer tvOS/Views/LiveTVHomeView.swift @@ -10,13 +10,14 @@ import Foundation import SwiftUI -struct LiveTVGuideView: View { - @EnvironmentObject var programsRouter: LiveTVGuideCoordinator.Router - @StateObject var viewModel = LiveTVGuideViewModel() - +struct LiveTVHomeView: View { + @EnvironmentObject var mainCoordinator: MainCoordinator.Router + var body: some View { Button {} label: { - Text("Coming Soon") + Text("Return Home") + }.onAppear { + self.mainCoordinator.root(\.mainTab) } } } diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index ccbca006..ec581680 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -248,19 +248,16 @@ C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; }; C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; }; C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; }; - C4BE077C272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */; }; - C4BE077D272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */; }; - C4BE0780272837FB003F4AD1 /* LiveTVGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */; }; - C4BE07822728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */; }; - C4BE07832728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */; }; C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; }; C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; }; C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; }; C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; }; C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; }; C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; }; + 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 */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; @@ -583,14 +580,15 @@ C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = ""; }; C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsViewModel.swift; sourceTree = ""; }; C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVTabCoordinator.swift; sourceTree = ""; }; - C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideViewModel.swift; sourceTree = ""; }; - C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideView.swift; sourceTree = ""; }; - C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVGuideCoordinator.swift; sourceTree = ""; }; C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsViewModel.swift; sourceTree = ""; }; C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsCoordinator.swift; sourceTree = ""; }; C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; + C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = ""; }; C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; + C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; + D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = ""; }; + DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = ""; }; E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = ""; }; @@ -746,7 +744,6 @@ 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, - C4BE077B272837C8003F4AD1 /* LiveTVGuideViewModel.swift */, C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */, @@ -872,6 +869,7 @@ isa = PBXGroup; children = ( 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, + C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */, E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */, 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */, 53116A18268B947A003024C9 /* PlainLinkButton.swift */, @@ -1136,7 +1134,6 @@ 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */, - C4BE07812728383F003F4AD1 /* LiveTVGuideCoordinator.swift */, C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */, C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, @@ -1215,8 +1212,8 @@ C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */, - C4BE077E272837FB003F4AD1 /* LiveTVGuideView.swift */, C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */, + C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */, C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */, 531690EE267ABF72005D8AB9 /* NextUpView.swift */, @@ -1823,16 +1820,15 @@ 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, - C4BE0780272837FB003F4AD1 /* LiveTVGuideView.swift in Sources */, 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, 62671DB327159C1800199D95 /* ItemCoordinator.swift in Sources */, + C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */, E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */, E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */, - C4BE07832728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */, 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, @@ -1862,6 +1858,7 @@ E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, + C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, @@ -1916,7 +1913,6 @@ 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, - C4BE077D272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */, @@ -1981,7 +1977,6 @@ 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, - C4BE077C272837C8003F4AD1 /* LiveTVGuideViewModel.swift in Sources */, 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, @@ -1997,7 +1992,6 @@ 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, - C4BE07822728383F003F4AD1 /* LiveTVGuideCoordinator.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, diff --git a/Shared/Coordinators/LiveTVGuideCoordinator.swift b/Shared/Coordinators/LiveTVGuideCoordinator.swift deleted file mode 100644 index 7e176f7c..00000000 --- a/Shared/Coordinators/LiveTVGuideCoordinator.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -/* - * 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 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Foundation -import JellyfinAPI -import Stinsen -import SwiftUI - -final class LiveTVGuideCoordinator: NavigationCoordinatable { - - let stack = NavigationStack(initial: \LiveTVGuideCoordinator.start) - - @Root var start = makeStart - @Route(.modal) var modalItem = makeModalItem - - func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { - return NavigationViewCoordinator(ItemCoordinator(item: item)) - } - - @ViewBuilder - func makeStart() -> some View { - LiveTVGuideView() - } -} diff --git a/Shared/Coordinators/LiveTVTabCoordinator.swift b/Shared/Coordinators/LiveTVTabCoordinator.swift index 971c83b5..6d0d28d8 100644 --- a/Shared/Coordinators/LiveTVTabCoordinator.swift +++ b/Shared/Coordinators/LiveTVTabCoordinator.swift @@ -13,14 +13,14 @@ import Stinsen final class LiveTVTabCoordinator: TabCoordinatable { var child = TabChild(startingItems: [ - \LiveTVTabCoordinator.programs, - \LiveTVTabCoordinator.guide, - \LiveTVTabCoordinator.channels + \LiveTVTabCoordinator.programs, + \LiveTVTabCoordinator.channels, + \LiveTVTabCoordinator.home ]) @Route(tabItem: makeProgramsTab) var programs = makePrograms - @Route(tabItem: makeGuideTab) var guide = makeGuide @Route(tabItem: makeChannelsTab) var channels = makeChannels + @Route(tabItem: makeHomeTab) var home = makeHome func makePrograms() -> NavigationViewCoordinator { return NavigationViewCoordinator(LiveTVProgramsCoordinator()) @@ -33,17 +33,6 @@ final class LiveTVTabCoordinator: TabCoordinatable { } } - func makeGuide() -> NavigationViewCoordinator { - return NavigationViewCoordinator(LiveTVGuideCoordinator()) - } - - @ViewBuilder func makeGuideTab(isActive: Bool) -> some View { - HStack { - Image(systemName: "calendar") - Text("Guide") - } - } - func makeChannels() -> NavigationViewCoordinator { return NavigationViewCoordinator(LiveTVChannelsCoordinator()) } @@ -54,4 +43,15 @@ final class LiveTVTabCoordinator: TabCoordinatable { Text("Channels") } } + + func makeHome() -> LiveTVHomeView { + return LiveTVHomeView() + } + + @ViewBuilder func makeHomeTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "house") + Text("Home") + } + } } diff --git a/Shared/ViewModels/LiveTVChannelsViewModel.swift b/Shared/ViewModels/LiveTVChannelsViewModel.swift index ce29d882..68922adb 100644 --- a/Shared/ViewModels/LiveTVChannelsViewModel.swift +++ b/Shared/ViewModels/LiveTVChannelsViewModel.swift @@ -9,7 +9,194 @@ import Foundation import JellyfinAPI +import SwiftUICollection + +typealias LiveTVChannelRow = CollectionRow + +struct LiveTVChannelRowCell: Hashable { + let id = UUID() + let item: LiveTVChannelProgram +} + +struct LiveTVChannelProgram: Hashable { + let id = UUID() + let channel: BaseItemDto + let program: BaseItemDto? +} final class LiveTVChannelsViewModel: ViewModel { + @Published var channels = [BaseItemDto]() + @Published var channelPrograms = [LiveTVChannelProgram]() { + didSet { + rows = [] + let rowChannels = channelPrograms.chunked(into: 4) + for (index, rowChans) in rowChannels.enumerated() { + rows.append(LiveTVChannelRow(section: index, items: rowChans.map { LiveTVChannelRowCell(item: $0) })) + } + } + } + @Published var rows = [LiveTVChannelRow]() + private var programs = [BaseItemDto]() + private var channelProgramsList = [BaseItemDto: [BaseItemDto]]() + + override init() { + super.init() + + getChannels() + } + + private func getGuideInfo() { + LiveTvAPI.getGuideInfo() + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received Guide Info") + guard let self = self else { return } + self.getChannels() + }) + .store(in: &cancellables) + } + + private func getChannels() { + LiveTvAPI.getLiveTvChannels( + userId: SessionManager.main.currentLogin.user.id, + startIndex: 0, + limit: 500, + enableImageTypes: [.primary], + enableUserData: false, + enableFavoriteSorting: true + ) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels") + guard let self = self else { return } + self.channels = response.items ?? [] + self.getPrograms() + }) + .store(in: &cancellables) + } + + private func getPrograms() { + // http://192.168.1.50:8096/LiveTv/Programs + guard channels.count > 0 else { + LogManager.shared.log.debug("Cannot get programs, channels list empty. ") + return + } + let channelIds = channels.compactMap { $0.id } + + let minEndDate = Date.now.addComponentsToDate(hours: -1) + let maxStartDate = minEndDate.addComponentsToDate(days: 1) + + NSLog("*** maxStartDate: \(maxStartDate)") + NSLog("*** minEndDate: \(minEndDate)") + + let getProgramsDto = GetProgramsDto( + channelIds: channelIds, + userId: SessionManager.main.currentLogin.user.id, + maxStartDate: maxStartDate, + minEndDate: minEndDate, + sortBy: ["StartDate"], + enableImages: true, + enableTotalRecordCount: false, + imageTypeLimit: 1, + enableImageTypes: [.primary], + enableUserData: false + ) + + LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Programs") + guard let self = self else { return } + self.programs = response.items ?? [] + self.channelPrograms = self.processChannelPrograms() + }) + .store(in: &cancellables) + } + + private func processChannelPrograms() -> [LiveTVChannelProgram] { + var channelPrograms = [LiveTVChannelProgram]() + let now = Date() + let df = DateFormatter() + df.dateFormat = "MM/dd h:mm ZZZ" + for channel in self.channels { + // NSLog("\n\(channel.name)") + let prgs = self.programs.filter { item in + item.channelId == channel.id + } + channelProgramsList[channel] = prgs + + var currentPrg: BaseItemDto? + for prg in prgs { + var startString = "" + var endString = "" + if let start = prg.startDate?.toLocalTime() { + startString = df.string(from: start) + } + if let end = prg.endDate?.toLocalTime() { + endString = df.string(from: end) + } + //NSLog("\(prg.name) - \(startString) to \(endString)") + if let startDate = prg.startDate?.toLocalTime() , + let endDate = prg.endDate?.toLocalTime(), + now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate && + now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate { + currentPrg = prg + } + } + + channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg)) + } + return channelPrograms + } +} + +extension Array { + func chunked(into size: Int) -> [[Element]] { + return stride(from: 0, to: count, by: size).map { + Array(self[$0 ..< Swift.min($0 + size, count)]) + } + } +} + +extension Date { + func addComponentsToDate(seconds sec: Int? = nil, minutes min: Int? = nil, hours hrs: Int? = nil, days d: Int? = nil) -> Date { + var dc = DateComponents() + if let sec = sec { + dc.second = sec + } + if let min = min { + dc.minute = min + } + if let hrs = hrs { + dc.hour = hrs + } + if let d = d { + dc.day = d + } + return Calendar.current.date(byAdding: dc, to: self)! + } + + func midnightUTCDate() -> Date { + var dc: DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: self) + dc.hour = 0 + dc.minute = 0 + dc.second = 0 + dc.nanosecond = 0 + dc.timeZone = TimeZone(secondsFromGMT: 0) + return Calendar.current.date(from: dc)! + } + + func toLocalTime() -> Date { + let timezoneOffset = TimeZone.current.secondsFromGMT() + let epochDate = self.timeIntervalSince1970 + let timezoneEpochOffset = (epochDate + Double(timezoneOffset)) + return Date(timeIntervalSince1970: timezoneEpochOffset) + } } diff --git a/Shared/ViewModels/LiveTVGuideViewModel.swift b/Shared/ViewModels/LiveTVGuideViewModel.swift deleted file mode 100644 index 4a2fa5ff..00000000 --- a/Shared/ViewModels/LiveTVGuideViewModel.swift +++ /dev/null @@ -1,15 +0,0 @@ -// - /* - * 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 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Foundation -import JellyfinAPI - -final class LiveTVGuideViewModel: ViewModel { - -} From 43cd1ecc624b281be6e49d7534f694158e182ef6 Mon Sep 17 00:00:00 2001 From: jhays Date: Thu, 18 Nov 2021 21:06:04 -0600 Subject: [PATCH 05/11] use coordinator to show player --- .../Components/MediaPlayButtonRowView.swift | 6 ++++-- JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.swift | 8 +++++++- .../Views/VideoPlayer/VideoPlayerViewController.swift | 3 ++- Shared/Coordinators/VideoPlayerCoordinator.swift | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift b/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift index 4e6ef971..fadbc207 100644 --- a/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift +++ b/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift @@ -10,14 +10,16 @@ import SwiftUI struct MediaPlayButtonRowView: View { - + @EnvironmentObject var itemRouter: ItemCoordinator.Router @ObservedObject var viewModel: ItemViewModel @State var wrappedScrollView: UIScrollView? var body: some View { HStack { VStack { - NavigationLink(destination: VideoPlayerView(item: viewModel.item).ignoresSafeArea()) { + Button { + self.itemRouter.route(to: \.videoPlayer, viewModel.item) + } label: { MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView) } Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString()) left" : L10n.play) diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.swift index 809ad75a..8e5b72fc 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayer.swift @@ -7,17 +7,23 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI import JellyfinAPI struct VideoPlayerView: UIViewControllerRepresentable { - var item: BaseItemDto + @EnvironmentObject var router: VideoPlayerCoordinator.Router + var item: BaseItemDto + func makeUIViewController(context: Context) -> some UIViewController { let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil) let viewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! VideoPlayerViewController viewController.manifest = item + viewController.backAction = { + self.router.dismissCoordinator() + } return viewController } diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift index e680ddaa..2f0a81d7 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift @@ -65,6 +65,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var playSessionId: String = "" + var backAction = {} var cancellables = Set() @@ -486,7 +487,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, self.resignFirstResponder() mediaPlayer.stop() sendStopReport() - self.navigationController?.popViewController(animated: true) + backAction() } } diff --git a/Shared/Coordinators/VideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator.swift index b4feb6fd..09f6f647 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator.swift @@ -26,5 +26,6 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { VideoPlayerView(item: item) + .ignoresSafeArea() } } From b5719e505f1666f6676e32bc3ea54541f5cc35e8 Mon Sep 17 00:00:00 2001 From: jhays Date: Fri, 19 Nov 2021 08:31:11 -0600 Subject: [PATCH 06/11] live tv playback, fix dates --- .../Components/LiveTVChannelItemElement.swift | 34 +++++++++---------- .../Views/LiveTVChannelsView.swift | 3 +- .../VideoPlayerViewController.swift | 4 +-- .../LiveTVChannelsCoordinator.swift | 4 +++ .../ViewModels/LiveTVChannelsViewModel.swift | 23 +++++-------- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift b/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift index 0e2037a9..fe83123c 100644 --- a/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift +++ b/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift @@ -47,29 +47,27 @@ struct LiveTVChannelItemElement: View { } if let currentProgram = program, - let start = currentProgram.startDate?.toLocalTime().timeIntervalSinceReferenceDate, - let end = currentProgram.endDate?.toLocalTime().timeIntervalSinceReferenceDate { + let startDate = currentProgram.startDate, + let endDate = currentProgram.endDate { + let start = startDate.timeIntervalSinceReferenceDate + let end = endDate.timeIntervalSinceReferenceDate let now = Date().timeIntervalSinceReferenceDate let length = end - start let progress = now - start let progPercent = progress / length - VStack { - if let startDate = currentProgram.startDate, - let endDate = currentProgram.endDate { - HStack { - Text(dateFormatter.string(from: startDate.toLocalTime())) - .font(.footnote) - .lineLimit(1) - .frame(alignment: .leading) - - Spacer() - - Text(dateFormatter.string(from: endDate.toLocalTime())) - .font(.footnote) - .lineLimit(1) - .frame(alignment: .trailing) - } + HStack { + Text(dateFormatter.string(from: startDate)) + .font(.footnote) + .lineLimit(1) + .frame(alignment: .leading) + + Spacer() + + Text(dateFormatter.string(from: endDate)) + .font(.footnote) + .lineLimit(1) + .frame(alignment: .trailing) } GeometryReader { gp in ZStack(alignment: .leading) { diff --git a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift index 8e2fd05d..d915543d 100644 --- a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift +++ b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift @@ -13,7 +13,7 @@ import SwiftUICollection struct LiveTVChannelsView: View { - @EnvironmentObject var programsRouter: LiveTVChannelsCoordinator.Router + @EnvironmentObject var router: LiveTVChannelsCoordinator.Router @StateObject var viewModel = LiveTVChannelsViewModel() var body: some View { @@ -49,6 +49,7 @@ struct LiveTVChannelsView: View { let channel = item.channel{ if channel.type != "Folder" { Button { + self.router.route(to: \.videoPlayer, channel) } label: { LiveTVChannelItemElement(channel: channel, program: item.program) } diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift index 2f0a81d7..f321138e 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerViewController.swift @@ -583,7 +583,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, updateNowPlayingCenter(time: nil, playing: mediaPlayer.state == .playing) if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { - var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)) + var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks ?? 0)) if ticks == 0 { ticks = manifest.userData?.playbackPositionTicks ?? 0 } @@ -601,7 +601,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } func sendStopReport() { - let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) + let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks ?? 0)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) .sink(receiveCompletion: { result in diff --git a/Shared/Coordinators/LiveTVChannelsCoordinator.swift b/Shared/Coordinators/LiveTVChannelsCoordinator.swift index 17b683d4..d2915f92 100644 --- a/Shared/Coordinators/LiveTVChannelsCoordinator.swift +++ b/Shared/Coordinators/LiveTVChannelsCoordinator.swift @@ -17,10 +17,14 @@ final class LiveTVChannelsCoordinator: NavigationCoordinatable { @Root var start = makeStart @Route(.modal) var modalItem = makeModalItem + @Route(.fullScreen) var videoPlayer = makeVideoPlayer func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { return NavigationViewCoordinator(ItemCoordinator(item: item)) } + func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) + } @ViewBuilder func makeStart() -> some View { diff --git a/Shared/ViewModels/LiveTVChannelsViewModel.swift b/Shared/ViewModels/LiveTVChannelsViewModel.swift index 68922adb..f90e573e 100644 --- a/Shared/ViewModels/LiveTVChannelsViewModel.swift +++ b/Shared/ViewModels/LiveTVChannelsViewModel.swift @@ -89,7 +89,7 @@ final class LiveTVChannelsViewModel: ViewModel { let channelIds = channels.compactMap { $0.id } let minEndDate = Date.now.addComponentsToDate(hours: -1) - let maxStartDate = minEndDate.addComponentsToDate(days: 1) + let maxStartDate = minEndDate.addComponentsToDate(hours: 6) NSLog("*** maxStartDate: \(maxStartDate)") NSLog("*** minEndDate: \(minEndDate)") @@ -125,8 +125,9 @@ final class LiveTVChannelsViewModel: ViewModel { let now = Date() let df = DateFormatter() df.dateFormat = "MM/dd h:mm ZZZ" + NSLog("begin processing programs") for channel in self.channels { - // NSLog("\n\(channel.name)") + NSLog("\n\(channel.name)") let prgs = self.programs.filter { item in item.channelId == channel.id } @@ -136,15 +137,15 @@ final class LiveTVChannelsViewModel: ViewModel { for prg in prgs { var startString = "" var endString = "" - if let start = prg.startDate?.toLocalTime() { + if let start = prg.startDate { startString = df.string(from: start) } - if let end = prg.endDate?.toLocalTime() { + if let end = prg.endDate { endString = df.string(from: end) } - //NSLog("\(prg.name) - \(startString) to \(endString)") - if let startDate = prg.startDate?.toLocalTime() , - let endDate = prg.endDate?.toLocalTime(), + NSLog("\(prg.name) - \(startString) to \(endString)") + if let startDate = prg.startDate, + let endDate = prg.endDate, now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate && now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate { currentPrg = prg @@ -153,6 +154,7 @@ final class LiveTVChannelsViewModel: ViewModel { channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg)) } + NSLog("finished processing programs") return channelPrograms } } @@ -192,11 +194,4 @@ extension Date { dc.timeZone = TimeZone(secondsFromGMT: 0) return Calendar.current.date(from: dc)! } - - func toLocalTime() -> Date { - let timezoneOffset = TimeZone.current.secondsFromGMT() - let epochDate = self.timeIntervalSince1970 - let timezoneEpochOffset = (epochDate + Double(timezoneOffset)) - return Date(timeIntervalSince1970: timezoneEpochOffset) - } } From ed57ea5577ae357787bb7ec17132997addf31c68 Mon Sep 17 00:00:00 2001 From: jhays Date: Sat, 20 Nov 2021 14:14:16 -0600 Subject: [PATCH 07/11] Cleanup and temp views for iOS --- .../Components/LandscapeItemElement.swift | 26 +++-- .../Components/LiveTVChannelItemElement.swift | 96 ------------------- .../Views/LiveTVChannelsView.swift | 9 +- JellyfinPlayer.xcodeproj/project.pbxproj | 16 +++- .../xcschemes/JellyfinPlayer tvOS.xcscheme | 78 +++++++++++++++ JellyfinPlayer/Views/LiveTVHomeView.swift | 15 +++ JellyfinPlayer/Views/LiveTVProgramsView.swift | 15 +++ .../BaseItemDtoExtensions.swift | 29 +++++- .../ViewModels/LiveTVChannelsViewModel.swift | 24 ++--- Shared/Views/LiveTVChannelItemElement.swift | 80 ++++++++++++++++ 10 files changed, 255 insertions(+), 133 deletions(-) delete mode 100644 JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift create mode 100644 JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer tvOS.xcscheme create mode 100644 JellyfinPlayer/Views/LiveTVHomeView.swift create mode 100644 JellyfinPlayer/Views/LiveTVProgramsView.swift create mode 100644 Shared/Views/LiveTVChannelItemElement.swift diff --git a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift index eb6b6a01..1da6bb5f 100644 --- a/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift +++ b/JellyfinPlayer tvOS/Components/LandscapeItemElement.swift @@ -91,22 +91,18 @@ struct LandscapeItemElement: View { ) .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) .shadow(radius: focused ? 10.0 : 0, y: focused ? 10.0 : 0) - if focused { - if inSeasonView ?? false { - Text("\(item.getEpisodeLocator() ?? "") • \(item.name ?? "")") - .font(.callout) - .fontWeight(.semibold) - .lineLimit(1) - .frame(width: 445) - } else { - Text(item.type == "Episode" ? "\(item.seriesName ?? "") • \(item.getEpisodeLocator() ?? "")" : item.name ?? "") - .font(.callout) - .fontWeight(.semibold) - .lineLimit(1) - .frame(width: 445) - } + if inSeasonView ?? false { + Text("\(item.getEpisodeLocator() ?? "") • \(item.name ?? "")") + .font(.callout) + .fontWeight(.semibold) + .lineLimit(1) + .frame(width: 445) } else { - Spacer().frame(height: 25) + Text(item.type == "Episode" ? "\(item.seriesName ?? "") • \(item.getEpisodeLocator() ?? "")" : item.name ?? "") + .font(.callout) + .fontWeight(.semibold) + .lineLimit(1) + .frame(width: 445) } } .onChange(of: envFocused) { envFocus in diff --git a/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift b/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift deleted file mode 100644 index fe83123c..00000000 --- a/JellyfinPlayer tvOS/Components/LiveTVChannelItemElement.swift +++ /dev/null @@ -1,96 +0,0 @@ -// - /* - * 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 2021 Aiden Vigue & Jellyfin Contributors - */ - - -import SwiftUI -import JellyfinAPI - -struct LiveTVChannelItemElement: View { - @Environment(\.isFocused) var envFocused: Bool - @State var focused: Bool = false - - var channel: BaseItemDto - var program: BaseItemDto? - - var dateFormatter: DateFormatter { - let df = DateFormatter() - df.dateFormat = "h:mm" - return df - } - - var body: some View { - VStack { - HStack { - Spacer() - Text(channel.number ?? "") - .font(.footnote) - .frame(alignment: .trailing) - }.frame(alignment: .top) - ImageView(src: channel.getPrimaryImage(maxWidth: 125)) - .frame(width: 125, alignment: .center) - .offset(x: 0, y: -32) - Text(channel.name ?? "?") - .font(.footnote) - .lineLimit(1) - .frame(alignment: .center) - if let currentProgram = program { - Text(currentProgram.name ?? "") - .font(.body) - .lineLimit(1) - .foregroundColor(.green) - } - - if let currentProgram = program, - let startDate = currentProgram.startDate, - let endDate = currentProgram.endDate { - let start = startDate.timeIntervalSinceReferenceDate - let end = endDate.timeIntervalSinceReferenceDate - let now = Date().timeIntervalSinceReferenceDate - let length = end - start - let progress = now - start - let progPercent = progress / length - VStack { - HStack { - Text(dateFormatter.string(from: startDate)) - .font(.footnote) - .lineLimit(1) - .frame(alignment: .leading) - - Spacer() - - Text(dateFormatter.string(from: endDate)) - .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(red: 172/255, green: 92/255, blue: 195/255)) - .frame(width: CGFloat(progPercent * gp.size.width), height: 12) - } - } - } - } - } - .padding() - .background(Color.clear) - .border(focused ? Color.blue : Color.clear, width: 4) - .onChange(of: envFocused) { envFocus in - withAnimation(.linear(duration: 0.15)) { - self.focused = envFocus - } - } - .scaleEffect(focused ? 1.1 : 1) - } -} diff --git a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift index d915543d..5599ddb5 100644 --- a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift +++ b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift @@ -8,6 +8,7 @@ */ import Foundation +import JellyfinAPI import SwiftUI import SwiftUICollection @@ -51,7 +52,13 @@ struct LiveTVChannelsView: View { Button { self.router.route(to: \.videoPlayer, channel) } label: { - LiveTVChannelItemElement(channel: channel, program: item.program) + LiveTVChannelItemElement( + channel: channel, + program: item.program, + startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ", + endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ", + progressPercent: item.program?.getLiveProgressPercentage() ?? 0 + ) } .buttonStyle(PlainNavigationLinkButtonStyle()) } diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index ec581680..a3852209 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -233,6 +233,9 @@ C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; }; C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; }; C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; + C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; }; + C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */; }; + C4AE2C3327498DBE00AE13CF /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; }; C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; }; @@ -572,6 +575,8 @@ C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = ""; }; C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = ""; }; C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = ""; }; + C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = ""; }; + C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = ""; }; C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesCoordinator.swift; sourceTree = ""; }; C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = ""; }; C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = ""; }; @@ -869,7 +874,6 @@ isa = PBXGroup; children = ( 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, - C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */, E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */, 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */, 53116A18268B947A003024C9 /* PlainLinkButton.swift */, @@ -1256,6 +1260,8 @@ 625CB56E2678C23300530A6E /* HomeView.swift */, E14F7D0A26DB3714007C3AE6 /* ItemView */, 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, + C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */, + C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */, 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, 6213388F265F83A900A81A2A /* LibraryListView.swift */, 53EE24E5265060780068F029 /* LibrarySearchView.swift */, @@ -1351,6 +1357,7 @@ E1AD105326D96F5A003E4A08 /* Views */ = { isa = PBXGroup; children = ( + C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */, 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, 621338B22660A07800A81A2A /* LazyView.swift */, @@ -1962,6 +1969,7 @@ 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, E19169CE272514760085832A /* HTTPScheme.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, + C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */, 0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */, @@ -1972,6 +1980,7 @@ 53892770263C25230035E14B /* NextUpView.swift in Sources */, 6264E88C273850380081A12A /* Strings.swift in Sources */, C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, + C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, @@ -1995,6 +2004,7 @@ 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, + C4AE2C3327498DBE00AE13CF /* LiveTVChannelItemElement.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */, @@ -2241,7 +2251,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = JM7WWM3V8C; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -2271,7 +2281,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = JM7WWM3V8C; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; diff --git a/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer tvOS.xcscheme b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer tvOS.xcscheme new file mode 100644 index 00000000..490b03f0 --- /dev/null +++ b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer tvOS.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JellyfinPlayer/Views/LiveTVHomeView.swift b/JellyfinPlayer/Views/LiveTVHomeView.swift new file mode 100644 index 00000000..bc7ded6f --- /dev/null +++ b/JellyfinPlayer/Views/LiveTVHomeView.swift @@ -0,0 +1,15 @@ +/* JellyfinPlayer/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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Stinsen +import SwiftUI + +struct LiveTVHomeView: View { + var body: some View { + Text("Coming Soon") + } +} diff --git a/JellyfinPlayer/Views/LiveTVProgramsView.swift b/JellyfinPlayer/Views/LiveTVProgramsView.swift new file mode 100644 index 00000000..9cb25845 --- /dev/null +++ b/JellyfinPlayer/Views/LiveTVProgramsView.swift @@ -0,0 +1,15 @@ +/* JellyfinPlayer/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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Stinsen +import SwiftUI + +struct LiveTVProgramsView: View { + var body: some View { + Text("Coming Soon") + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 2f27801d..70937001 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -170,7 +170,34 @@ public extension BaseItemDto { return "\(String(progminutes))m" } } - + + func getLiveStartTimeString(formatter: DateFormatter) -> String { + if let startDate = self.startDate { + return formatter.string(from: startDate) + } + return " " + } + + func getLiveEndTimeString(formatter: DateFormatter) -> String { + if let endDate = self.endDate { + return formatter.string(from: endDate) + } + return " " + } + + func getLiveProgressPercentage() -> Double { + if let startDate = self.startDate, + let endDate = self.endDate { + let start = startDate.timeIntervalSinceReferenceDate + let end = endDate.timeIntervalSinceReferenceDate + let now = Date().timeIntervalSinceReferenceDate + let length = end - start + let progress = now - start + return progress / length + } + return 0 + } + // MARK: ItemType enum ItemType: String { diff --git a/Shared/ViewModels/LiveTVChannelsViewModel.swift b/Shared/ViewModels/LiveTVChannelsViewModel.swift index f90e573e..e20df2ee 100644 --- a/Shared/ViewModels/LiveTVChannelsViewModel.swift +++ b/Shared/ViewModels/LiveTVChannelsViewModel.swift @@ -37,9 +37,16 @@ final class LiveTVChannelsViewModel: ViewModel { } } @Published var rows = [LiveTVChannelRow]() + private var programs = [BaseItemDto]() private var channelProgramsList = [BaseItemDto: [BaseItemDto]]() + var timeFormatter: DateFormatter { + let df = DateFormatter() + df.dateFormat = "h:mm" + return df + } + override init() { super.init() @@ -91,9 +98,6 @@ final class LiveTVChannelsViewModel: ViewModel { let minEndDate = Date.now.addComponentsToDate(hours: -1) let maxStartDate = minEndDate.addComponentsToDate(hours: 6) - NSLog("*** maxStartDate: \(maxStartDate)") - NSLog("*** minEndDate: \(minEndDate)") - let getProgramsDto = GetProgramsDto( channelIds: channelIds, userId: SessionManager.main.currentLogin.user.id, @@ -123,11 +127,7 @@ final class LiveTVChannelsViewModel: ViewModel { private func processChannelPrograms() -> [LiveTVChannelProgram] { var channelPrograms = [LiveTVChannelProgram]() let now = Date() - let df = DateFormatter() - df.dateFormat = "MM/dd h:mm ZZZ" - NSLog("begin processing programs") for channel in self.channels { - NSLog("\n\(channel.name)") let prgs = self.programs.filter { item in item.channelId == channel.id } @@ -135,15 +135,6 @@ final class LiveTVChannelsViewModel: ViewModel { var currentPrg: BaseItemDto? for prg in prgs { - var startString = "" - var endString = "" - if let start = prg.startDate { - startString = df.string(from: start) - } - if let end = prg.endDate { - endString = df.string(from: end) - } - NSLog("\(prg.name) - \(startString) to \(endString)") if let startDate = prg.startDate, let endDate = prg.endDate, now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate && @@ -154,7 +145,6 @@ final class LiveTVChannelsViewModel: ViewModel { channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg)) } - NSLog("finished processing programs") return channelPrograms } } diff --git a/Shared/Views/LiveTVChannelItemElement.swift b/Shared/Views/LiveTVChannelItemElement.swift new file mode 100644 index 00000000..2f4e2126 --- /dev/null +++ b/Shared/Views/LiveTVChannelItemElement.swift @@ -0,0 +1,80 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + + +import SwiftUI +import JellyfinAPI + +struct LiveTVChannelItemElement: View { + @Environment(\.isFocused) var envFocused: Bool + @State var focused: Bool = false + + var channel: BaseItemDto + var program: BaseItemDto? + var startString = " " + var endString = " " + var progressPercent = Double(0) + + var body: some View { + VStack { + HStack { + Spacer() + Text(channel.number ?? "") + .font(.footnote) + .frame(alignment: .trailing) + }.frame(alignment: .top) + ImageView(src: channel.getPrimaryImage(maxWidth: 125)) + .frame(width: 125, alignment: .center) + .offset(x: 0, y: -32) + Text(channel.name ?? "?") + .font(.footnote) + .lineLimit(1) + .frame(alignment: .center) + Text(program?.name ?? "N/A") + .font(.body) + .lineLimit(1) + .foregroundColor(.green) + VStack { + 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(red: 172/255, green: 92/255, blue: 195/255)) + .frame(width: CGFloat(progressPercent * gp.size.width), height: 12) + } + } + } + } + .padding() + .background(Color.clear) + .border(focused ? Color.blue : Color.clear, width: 4) + .onChange(of: envFocused) { envFocus in + withAnimation(.linear(duration: 0.15)) { + self.focused = envFocus + } + } + .scaleEffect(focused ? 1.1 : 1) + } +} From a4d58e3694a93c04b9b05f739e2f45a9465813ed Mon Sep 17 00:00:00 2001 From: jhays Date: Sat, 20 Nov 2021 14:15:26 -0600 Subject: [PATCH 08/11] remove team ID --- JellyfinPlayer.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index a3852209..0f474e82 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -2251,7 +2251,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = JM7WWM3V8C; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -2281,7 +2281,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = JM7WWM3V8C; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; From c98f63c5e41f63435b8bc80799d6c9b366967c7d Mon Sep 17 00:00:00 2001 From: jhays Date: Sat, 20 Nov 2021 14:57:20 -0600 Subject: [PATCH 09/11] support playback on programs view --- .../Views/LiveTVProgramsView.swift | 30 ++++++++-- .../LiveTVChannelsCoordinator.swift | 1 + .../LiveTVProgramsCoordinator.swift | 6 +- .../ViewModels/LiveTVChannelsViewModel.swift | 2 +- .../ViewModels/LiveTVProgramsViewModel.swift | 57 +++++++++++++++---- 5 files changed, 74 insertions(+), 22 deletions(-) diff --git a/JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift b/JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift index f979848b..acefbada 100644 --- a/JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift +++ b/JellyfinPlayer tvOS/Views/LiveTVProgramsView.swift @@ -28,7 +28,10 @@ struct LiveTVProgramsView: View { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in Button { - self.programsRouter.route(to: \.modalItem, item) + if let chanId = item.channelId, + let chan = viewModel.findChannel(id: chanId) { + self.programsRouter.route(to: \.videoPlayer, chan) + } } label: { LandscapeItemElement(item: item) } @@ -49,7 +52,10 @@ struct LiveTVProgramsView: View { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in Button { - self.programsRouter.route(to: \.modalItem, item) + if let chanId = item.channelId, + let chan = viewModel.findChannel(id: chanId) { + self.programsRouter.route(to: \.videoPlayer, chan) + } } label: { LandscapeItemElement(item: item) } @@ -70,7 +76,10 @@ struct LiveTVProgramsView: View { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in Button { - self.programsRouter.route(to: \.modalItem, item) + if let chanId = item.channelId, + let chan = viewModel.findChannel(id: chanId) { + self.programsRouter.route(to: \.videoPlayer, chan) + } } label: { LandscapeItemElement(item: item) } @@ -91,7 +100,10 @@ struct LiveTVProgramsView: View { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in Button { - self.programsRouter.route(to: \.modalItem, item) + if let chanId = item.channelId, + let chan = viewModel.findChannel(id: chanId) { + self.programsRouter.route(to: \.videoPlayer, chan) + } } label: { LandscapeItemElement(item: item) } @@ -112,7 +124,10 @@ struct LiveTVProgramsView: View { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in Button { - self.programsRouter.route(to: \.modalItem, item) + if let chanId = item.channelId, + let chan = viewModel.findChannel(id: chanId) { + self.programsRouter.route(to: \.videoPlayer, chan) + } } label: { LandscapeItemElement(item: item) } @@ -133,7 +148,10 @@ struct LiveTVProgramsView: View { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in Button { - self.programsRouter.route(to: \.modalItem, item) + if let chanId = item.channelId, + let chan = viewModel.findChannel(id: chanId) { + self.programsRouter.route(to: \.videoPlayer, chan) + } } label: { LandscapeItemElement(item: item) } diff --git a/Shared/Coordinators/LiveTVChannelsCoordinator.swift b/Shared/Coordinators/LiveTVChannelsCoordinator.swift index d2915f92..f2a6e482 100644 --- a/Shared/Coordinators/LiveTVChannelsCoordinator.swift +++ b/Shared/Coordinators/LiveTVChannelsCoordinator.swift @@ -22,6 +22,7 @@ final class LiveTVChannelsCoordinator: NavigationCoordinatable { func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { return NavigationViewCoordinator(ItemCoordinator(item: item)) } + func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator { NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) } diff --git a/Shared/Coordinators/LiveTVProgramsCoordinator.swift b/Shared/Coordinators/LiveTVProgramsCoordinator.swift index c500697e..1dd26daf 100644 --- a/Shared/Coordinators/LiveTVProgramsCoordinator.swift +++ b/Shared/Coordinators/LiveTVProgramsCoordinator.swift @@ -17,10 +17,10 @@ final class LiveTVProgramsCoordinator: NavigationCoordinatable { let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start) @Root var start = makeStart - @Route(.modal) var modalItem = makeModalItem + @Route(.fullScreen) var videoPlayer = makeVideoPlayer - func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { - return NavigationViewCoordinator(ItemCoordinator(item: item)) + func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) } @ViewBuilder diff --git a/Shared/ViewModels/LiveTVChannelsViewModel.swift b/Shared/ViewModels/LiveTVChannelsViewModel.swift index e20df2ee..f4899e8f 100644 --- a/Shared/ViewModels/LiveTVChannelsViewModel.swift +++ b/Shared/ViewModels/LiveTVChannelsViewModel.swift @@ -70,7 +70,7 @@ final class LiveTVChannelsViewModel: ViewModel { LiveTvAPI.getLiveTvChannels( userId: SessionManager.main.currentLogin.user.id, startIndex: 0, - limit: 500, + limit: 1000, enableImageTypes: [.primary], enableUserData: false, enableFavoriteSorting: true diff --git a/Shared/ViewModels/LiveTVProgramsViewModel.swift b/Shared/ViewModels/LiveTVProgramsViewModel.swift index 29023213..862e3534 100644 --- a/Shared/ViewModels/LiveTVProgramsViewModel.swift +++ b/Shared/ViewModels/LiveTVProgramsViewModel.swift @@ -19,18 +19,51 @@ final class LiveTVProgramsViewModel: ViewModel { @Published var kidsItems = [BaseItemDto]() @Published var newsItems = [BaseItemDto]() + private var channels = [String:BaseItemDto]() + override init() { super.init() - loadRecommendedPrograms() - loadSeries() - loadMovies() - loadSports() - loadKids() - loadNews() + getChannels() } - private func loadRecommendedPrograms() { + func findChannel(id: String) -> BaseItemDto? { + return channels[id] + } + + private func getChannels() { + LiveTvAPI.getLiveTvChannels( + userId: SessionManager.main.currentLogin.user.id, + startIndex: 0, + limit: 1000, + enableImageTypes: [.primary], + enableUserData: false, + enableFavoriteSorting: true + ) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels") + guard let self = self else { return } + if let chans = response.items { + for chan in chans { + if let chanId = chan.id { + self.channels[chanId] = chan + } + } + self.getRecommendedPrograms() + self.getSeries() + self.getMovies() + self.getSports() + self.getKids() + self.getNews() + } + }) + .store(in: &cancellables) + } + + private func getRecommendedPrograms() { LiveTvAPI.getRecommendedPrograms( userId: SessionManager.main.currentLogin.user.id, limit: 9, @@ -51,7 +84,7 @@ final class LiveTVProgramsViewModel: ViewModel { .store(in: &cancellables) } - private func loadSeries() { + private func getSeries() { let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, hasAired: false, isMovie: false, @@ -77,7 +110,7 @@ final class LiveTVProgramsViewModel: ViewModel { .store(in: &cancellables) } - private func loadMovies() { + private func getMovies() { let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, hasAired: false, isMovie: true, @@ -103,7 +136,7 @@ final class LiveTVProgramsViewModel: ViewModel { .store(in: &cancellables) } - private func loadSports() { + private func getSports() { let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, hasAired: false, isSports: true, @@ -125,7 +158,7 @@ final class LiveTVProgramsViewModel: ViewModel { .store(in: &cancellables) } - private func loadKids() { + private func getKids() { let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, hasAired: false, isKids: true, @@ -147,7 +180,7 @@ final class LiveTVProgramsViewModel: ViewModel { .store(in: &cancellables) } - private func loadNews() { + private func getNews() { let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, hasAired: false, isNews: true, From 1bafa774de336023c4f0993c6b98996374d56039 Mon Sep 17 00:00:00 2001 From: jhays Date: Tue, 23 Nov 2021 08:19:03 -0600 Subject: [PATCH 10/11] minor feedback cleanup --- .../Views/LibraryListView.swift | 4 +-- .../Views/LiveTVChannelsView.swift | 33 +++++++++---------- .../ViewModels/LiveTVChannelsViewModel.swift | 2 +- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/JellyfinPlayer tvOS/Views/LibraryListView.swift b/JellyfinPlayer tvOS/Views/LibraryListView.swift index 7c50212f..57e9efae 100644 --- a/JellyfinPlayer tvOS/Views/LibraryListView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryListView.swift @@ -32,8 +32,6 @@ struct LibraryListView: View { } label: { ZStack { - ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) - .opacity(0.4) HStack { Spacer() VStack { @@ -44,7 +42,7 @@ struct LibraryListView: View { } Spacer() }.padding(32) - }.background(Color.black) + } .frame(minWidth: 100, maxWidth: .infinity) .frame(height: 100) } diff --git a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift index 5599ddb5..1068225b 100644 --- a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift +++ b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift @@ -36,7 +36,7 @@ struct LiveTVChannelsView: View { VStack { Text("No results.") Button { - print("movieLibraries reload") + viewModel.getChannels() } label: { Text("Reload") } @@ -45,24 +45,21 @@ struct LiveTVChannelsView: View { } @ViewBuilder func makeCellView(indexPath: IndexPath, cell: LiveTVChannelRowCell) -> some View { - GeometryReader { _ in - if let item = cell.item, - let channel = item.channel{ - if channel.type != "Folder" { - Button { - self.router.route(to: \.videoPlayer, channel) - } label: { - LiveTVChannelItemElement( - channel: channel, - program: item.program, - startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ", - endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ", - progressPercent: item.program?.getLiveProgressPercentage() ?? 0 - ) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - } + let item = cell.item + let channel = item.channel + if channel.type != "Folder" { + Button { + self.router.route(to: \.videoPlayer, channel) + } label: { + LiveTVChannelItemElement( + channel: channel, + program: item.program, + startString: item.program?.getLiveStartTimeString(formatter: viewModel.timeFormatter) ?? " ", + endString: item.program?.getLiveEndTimeString(formatter: viewModel.timeFormatter) ?? " ", + progressPercent: item.program?.getLiveProgressPercentage() ?? 0 + ) } + .buttonStyle(PlainNavigationLinkButtonStyle()) } } diff --git a/Shared/ViewModels/LiveTVChannelsViewModel.swift b/Shared/ViewModels/LiveTVChannelsViewModel.swift index f4899e8f..fb92cc1d 100644 --- a/Shared/ViewModels/LiveTVChannelsViewModel.swift +++ b/Shared/ViewModels/LiveTVChannelsViewModel.swift @@ -66,7 +66,7 @@ final class LiveTVChannelsViewModel: ViewModel { .store(in: &cancellables) } - private func getChannels() { + func getChannels() { LiveTvAPI.getLiveTvChannels( userId: SessionManager.main.currentLogin.user.id, startIndex: 0, From 98a11ce3038cdec0f88e65a2fa64007fe9b5a623 Mon Sep 17 00:00:00 2001 From: jhays Date: Wed, 22 Dec 2021 12:23:51 -0600 Subject: [PATCH 11/11] refresh programs on channel view every 10 mins --- .../Views/LiveTVChannelsView.swift | 7 ++- .../ViewModels/LiveTVChannelsViewModel.swift | 44 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift index 1068225b..69f34e51 100644 --- a/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift +++ b/JellyfinPlayer tvOS/Views/LiveTVChannelsView.swift @@ -31,7 +31,12 @@ struct LiveTVChannelsView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() - + .onAppear { + viewModel.startScheduleCheckTimer() + } + .onDisappear { + viewModel.stopScheduleCheckTimer() + } } else { VStack { Text("No results.") diff --git a/Shared/ViewModels/LiveTVChannelsViewModel.swift b/Shared/ViewModels/LiveTVChannelsViewModel.swift index fb92cc1d..626f3924 100644 --- a/Shared/ViewModels/LiveTVChannelsViewModel.swift +++ b/Shared/ViewModels/LiveTVChannelsViewModel.swift @@ -40,6 +40,7 @@ final class LiveTVChannelsViewModel: ViewModel { private var programs = [BaseItemDto]() private var channelProgramsList = [BaseItemDto: [BaseItemDto]]() + private var timer: Timer? var timeFormatter: DateFormatter { let df = DateFormatter() @@ -51,6 +52,11 @@ final class LiveTVChannelsViewModel: ViewModel { super.init() getChannels() + startScheduleCheckTimer() + } + + deinit { + stopScheduleCheckTimer() } private func getGuideInfo() { @@ -131,7 +137,9 @@ final class LiveTVChannelsViewModel: ViewModel { let prgs = self.programs.filter { item in item.channelId == channel.id } - channelProgramsList[channel] = prgs + DispatchQueue.main.async { + self.channelProgramsList[channel] = prgs + } var currentPrg: BaseItemDto? for prg in prgs { @@ -147,6 +155,40 @@ final class LiveTVChannelsViewModel: ViewModel { } return channelPrograms } + + func startScheduleCheckTimer() { + let date = Date() + let calendar = Calendar.current + var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute], from: date) + + // Run on 10th min of every hour + guard let minute = components.minute else { return } + components.second = 0 + components.minute = minute + (10 - (minute % 10)) + + guard let nextMinute = calendar.date(from: components) else { return } + + if let existingTimer = timer { + existingTimer.invalidate() + } + timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] timer in + guard let self = self else { return } + LogManager.shared.log.debug("LiveTVChannels schedule check...") + DispatchQueue.global(qos: .background).async { + let newChanPrgs = self.processChannelPrograms() + DispatchQueue.main.async { + self.channelPrograms = newChanPrgs + } + } + } + if let timer = timer { + RunLoop.main.add(timer, forMode: .default) + } + } + + func stopScheduleCheckTimer() { + timer?.invalidate() + } } extension Array {