From 1fded3ee8ed356507f9fc62ae1e4bf9712f3016e Mon Sep 17 00:00:00 2001 From: jhays Date: Tue, 26 Oct 2021 09:06:11 -0500 Subject: [PATCH] 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) + } +}