diff --git a/JellyfinPlayer tvOS/Views/ItemView/EpisodesRowView.swift b/JellyfinPlayer tvOS/Components/EpisodesRowView.swift similarity index 100% rename from JellyfinPlayer tvOS/Views/ItemView/EpisodesRowView.swift rename to JellyfinPlayer tvOS/Components/EpisodesRowView.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/ItemDetailsView.swift b/JellyfinPlayer tvOS/Components/ItemDetailsView.swift similarity index 100% rename from JellyfinPlayer tvOS/Views/ItemView/ItemDetailsView.swift rename to JellyfinPlayer tvOS/Components/ItemDetailsView.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/PortraitItemsRowView.swift b/JellyfinPlayer tvOS/Components/PortraitItemsRowView.swift similarity index 98% rename from JellyfinPlayer tvOS/Views/ItemView/PortraitItemsRowView.swift rename to JellyfinPlayer tvOS/Components/PortraitItemsRowView.swift index 72c69de2..f2711fe1 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/PortraitItemsRowView.swift +++ b/JellyfinPlayer tvOS/Components/PortraitItemsRowView.swift @@ -63,5 +63,6 @@ struct PortraitItemsRowView: View { } .edgesIgnoringSafeArea(.horizontal) } + .focusSection() } } diff --git a/JellyfinPlayer tvOS/Components/SingleSeasonEpisodesRowView.swift b/JellyfinPlayer tvOS/Components/SingleSeasonEpisodesRowView.swift new file mode 100644 index 00000000..55bba152 --- /dev/null +++ b/JellyfinPlayer tvOS/Components/SingleSeasonEpisodesRowView.swift @@ -0,0 +1,122 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import JellyfinAPI +import SwiftUI + +struct SingleSeasonEpisodesRowView: View { + + @EnvironmentObject var itemRouter: ItemCoordinator.Router + @ObservedObject var viewModel: SingleSeasonEpisodesRowViewModel + + var body: some View { + VStack(alignment: .leading) { + + Text("Episodes") + .font(.title3) + .padding(.horizontal, 50) + + ScrollView(.horizontal) { + ScrollViewReader { reader in + HStack(alignment: .top) { + if viewModel.isLoading { + VStack(alignment: .leading) { + + ZStack { + Color.secondary.ignoresSafeArea() + + ProgressView() + } + .mask(Rectangle().frame(width: 500, height: 280)) + .frame(width: 500, height: 280) + + VStack(alignment: .leading) { + Text("S-E-") + .font(.caption) + .foregroundColor(.secondary) + Text("--") + .font(.footnote) + .padding(.bottom, 1) + Text("--") + .font(.caption) + .fontWeight(.light) + .lineLimit(4) + } + .padding(.horizontal) + + Spacer() + } + .frame(width: 500) + .focusable() + } else if viewModel.episodes.isEmpty { + VStack(alignment: .leading) { + + Color.secondary + .mask(Rectangle().frame(width: 500, height: 280)) + .frame(width: 500, height: 280) + + VStack(alignment: .leading) { + Text("--") + .font(.caption) + .foregroundColor(.secondary) + Text("No episodes available") + .font(.footnote) + .padding(.bottom, 1) + } + .padding(.horizontal) + + Spacer() + } + .frame(width: 500) + .focusable() + } else { + ForEach(viewModel.episodes, id:\.self) { episode in + Button { + itemRouter.route(to: \.item, episode) + } label: { + HStack(alignment: .top) { + VStack(alignment: .leading) { + + ImageView(src: episode.getBackdropImage(maxWidth: 445), + bh: episode.getBackdropImageBlurHash()) + .mask(Rectangle().frame(width: 500, height: 280)) + .frame(width: 500, height: 280) + + VStack(alignment: .leading) { + Text(episode.getEpisodeLocator() ?? "") + .font(.caption) + .foregroundColor(.secondary) + Text(episode.name ?? "") + .font(.footnote) + .padding(.bottom, 1) + Text(episode.overview ?? "") + .font(.caption) + .fontWeight(.light) + .lineLimit(4) + } + .padding(.horizontal) + + Spacer() + } + .frame(width: 500) + } + } + .buttonStyle(PlainButtonStyle()) + .id(episode.name) + } + } + } + .padding(.horizontal, 50) + .padding(.vertical) + } + .edgesIgnoringSafeArea(.horizontal) + } + } + } +} diff --git a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicEpisodeItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicEpisodeItemView.swift index 915099c9..8d30bc43 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicEpisodeItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicEpisodeItemView.swift @@ -18,6 +18,14 @@ struct CinematicEpisodeItemView: View { @State var wrappedScrollView: UIScrollView? @Default(.showPosterLabels) var showPosterLabels + func generateSubtitle() -> String? { + guard let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.getEpisodeLocator() else { + return nil + } + + return "\(seriesName) - \(episodeLocator)" + } + var body: some View { ZStack { @@ -28,7 +36,10 @@ struct CinematicEpisodeItemView: View { ScrollView { VStack(spacing: 0) { - CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView) + CinematicItemViewTopRow(viewModel: viewModel, + wrappedScrollView: wrappedScrollView, + title: viewModel.item.name ?? "", + subtitle: generateSubtitle()) .focusSection() .frame(height: UIScreen.main.bounds.height - 10) @@ -44,6 +55,13 @@ struct CinematicEpisodeItemView: View { EpisodesRowView(viewModel: EpisodesRowViewModel(episodeItemViewModel: viewModel)) .focusSection() + if let seriesItem = viewModel.series { + PortraitItemsRowView(rowTitle: "Series", + items: [seriesItem]) { seriesItem in + itemRouter.route(to: \.item, seriesItem) + } + } + if !viewModel.similarItems.isEmpty { PortraitItemsRowView(rowTitle: "Recommended", items: viewModel.similarItems, diff --git a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift index 71c44ec3..e6c51402 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift @@ -16,6 +16,8 @@ struct CinematicItemViewTopRow: View { @Environment(\.isFocused) var envFocused: Bool @State var focused: Bool = false @State var wrappedScrollView: UIScrollView? + @State var title: String + @State var subtitle: String? var body: some View { ZStack(alignment: .bottom) { @@ -34,7 +36,11 @@ struct CinematicItemViewTopRow: View { // MARK: Play Button { - itemRouter.route(to: \.videoPlayer, viewModel.itemVideoPlayerViewModel!) + if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel { + itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel) + } else { + LogManager.shared.log.error("Attempted to play item but no playback information available") + } } label: { HStack(spacing: 15) { Image(systemName: "play.fill") @@ -42,48 +48,46 @@ struct CinematicItemViewTopRow: View { .font(.title3) Text(viewModel.playButtonText()) .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black) -// .font(.title3) .fontWeight(.semibold) } .frame(width: 230, height: 100) .background(viewModel.playButtonItem == nil ? Color.secondarySystemFill : Color.white) .cornerRadius(10) - -// ZStack { -// Color.white.frame(width: 230, height: 100) -// -// Text("Play") -// .font(.title3) -// .foregroundColor(.black) -// } } .buttonStyle(CardButtonStyle()) - .disabled(viewModel.itemVideoPlayerViewModel == nil) } } VStack(alignment: .leading, spacing: 5) { - Text(viewModel.item.name ?? "") + Text(title) .font(.title2) .lineLimit(2) - if let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.getEpisodeLocator() { - Text("\(seriesName) - \(episodeLocator)") + if let subtitle = subtitle { + Text(subtitle) } HStack(alignment: .PlayInformationAlignmentGuide, spacing: 20) { - if let runtime = viewModel.item.getItemRuntime() { - Text(runtime) - .font(.subheadline) - .fontWeight(.medium) - } - - if let productionYear = viewModel.item.productionYear { - Text(String(productionYear)) - .font(.subheadline) - .fontWeight(.medium) - .lineLimit(1) + if viewModel.item.itemType == .series { + if let airTime = viewModel.item.airTime { + Text(airTime) + .font(.subheadline) + .fontWeight(.medium) + } + } else { + if let runtime = viewModel.item.getItemRuntime() { + Text(runtime) + .font(.subheadline) + .fontWeight(.medium) + } + + if let productionYear = viewModel.item.productionYear { + Text(String(productionYear)) + .font(.subheadline) + .fontWeight(.medium) + .lineLimit(1) + } } if let officialRating = viewModel.item.officialRating { @@ -121,17 +125,6 @@ struct CinematicItemViewTopRow: View { } } -extension HorizontalAlignment { - - private struct TitleSubtitleAlignment: AlignmentID { - static func defaultValue(in context: ViewDimensions) -> CGFloat { - context[HorizontalAlignment.leading] - } - } - - static let EpisodeSeriesAlignmentGuide = HorizontalAlignment(TitleSubtitleAlignment.self) -} - extension VerticalAlignment { private struct PlayInformationAlignment: AlignmentID { diff --git a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift index 42d24639..0ca3865d 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicMovieItemView.swift @@ -28,7 +28,10 @@ struct CinematicMovieItemView: View { ScrollView { VStack(spacing: 0) { - CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView) + CinematicItemViewTopRow(viewModel: viewModel, + wrappedScrollView: wrappedScrollView, + title: viewModel.item.name ?? "", + subtitle: nil) .focusSection() .frame(height: UIScreen.main.bounds.height - 10) diff --git a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicSeasonItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicSeasonItemView.swift new file mode 100644 index 00000000..c527ce65 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicSeasonItemView.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 Defaults +import SwiftUI + +struct CinematicSeasonItemView: View { + + @EnvironmentObject var itemRouter: ItemCoordinator.Router + @ObservedObject var viewModel: SeasonItemViewModel + @State var wrappedScrollView: UIScrollView? + @Default(.showPosterLabels) var showPosterLabels + + var body: some View { + ZStack { + + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash()) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 0) { + + if let seriesItem = viewModel.seriesItem { + CinematicItemViewTopRow(viewModel: viewModel, + wrappedScrollView: wrappedScrollView, + title: viewModel.item.name ?? "", + subtitle: seriesItem.name) + .focusSection() + .frame(height: UIScreen.main.bounds.height - 10) + } else { + CinematicItemViewTopRow(viewModel: viewModel, + wrappedScrollView: wrappedScrollView, + title: viewModel.item.name ?? "") + .focusSection() + .frame(height: UIScreen.main.bounds.height - 10) + } + + ZStack(alignment: .topLeading) { + + Color.black.ignoresSafeArea() + .frame(minHeight: UIScreen.main.bounds.height) + + VStack(alignment: .leading, spacing: 20) { + + CinematicItemAboutView(viewModel: viewModel) + + SingleSeasonEpisodesRowView(viewModel: SingleSeasonEpisodesRowViewModel(seasonItemViewModel: viewModel)) + + if let seriesItem = viewModel.seriesItem { + PortraitItemsRowView(rowTitle: "Series", + items: [seriesItem]) { seriesItem in + itemRouter.route(to: \.item, seriesItem) + } + } + + if !viewModel.similarItems.isEmpty { + PortraitItemsRowView(rowTitle: "Recommended", + items: viewModel.similarItems, + showItemTitles: showPosterLabels) { item in + itemRouter.route(to: \.item, item) + } + } + } + .padding(.vertical, 50) + } + } + } + .introspectScrollView { scrollView in + wrappedScrollView = scrollView + } + .ignoresSafeArea() + } + } +} diff --git a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicSeriesItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicSeriesItemView.swift new file mode 100644 index 00000000..6ff5e24e --- /dev/null +++ b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicSeriesItemView.swift @@ -0,0 +1,69 @@ +// + /* + * 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 Defaults +import SwiftUI + +struct CinematicSeriesItemView: View { + + @EnvironmentObject var itemRouter: ItemCoordinator.Router + @ObservedObject var viewModel: SeriesItemViewModel + @State var wrappedScrollView: UIScrollView? + @Default(.showPosterLabels) var showPosterLabels + + var body: some View { + ZStack { + + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), bh: viewModel.item.getBackdropImageBlurHash()) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 0) { + + CinematicItemViewTopRow(viewModel: viewModel, + wrappedScrollView: wrappedScrollView, + title: viewModel.item.name ?? "", + subtitle: nil) + .focusSection() + .frame(height: UIScreen.main.bounds.height - 10) + + ZStack(alignment: .topLeading) { + + Color.black.ignoresSafeArea() + .frame(minHeight: UIScreen.main.bounds.height) + + VStack(alignment: .leading, spacing: 20) { + + CinematicItemAboutView(viewModel: viewModel) + + PortraitItemsRowView(rowTitle: "Seasons", + items: viewModel.seasons, + showItemTitles: showPosterLabels) { season in + itemRouter.route(to: \.item, season) + } + + if !viewModel.similarItems.isEmpty { + PortraitItemsRowView(rowTitle: "Recommended", + items: viewModel.similarItems, + showItemTitles: showPosterLabels) { item in + itemRouter.route(to: \.item, item) + } + } + } + .padding(.vertical, 50) + } + } + } + .introspectScrollView { scrollView in + wrappedScrollView = scrollView + } + .ignoresSafeArea() + } + } +} diff --git a/JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CompactItemView/EpisodeItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/Views/ItemView/EpisodeItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/CompactItemView/EpisodeItemView.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/MovieItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CompactItemView/MovieItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/Views/ItemView/MovieItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/CompactItemView/MovieItemView.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CompactItemView/SeasonItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/Views/ItemView/SeasonItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/CompactItemView/SeasonItemView.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CompactItemView/SeriesItemView.swift similarity index 100% rename from JellyfinPlayer tvOS/Views/ItemView/SeriesItemView.swift rename to JellyfinPlayer tvOS/Views/ItemView/CompactItemView/SeriesItemView.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift index cc523200..ad443c65 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift @@ -25,8 +25,7 @@ struct ItemNavigationView: View { struct ItemView: View { - @Default(.tvOSEpisodeItemCinematicView) var tvOSEpisodeItemCinematicView - @Default(.tvOSMovieItemCinematicView) var tvOSMovieItemCinematicView + @Default(.tvOSCinematicViews) var tvOSCinematicViews private var item: BaseItemDto @@ -36,23 +35,32 @@ struct ItemView: View { var body: some View { Group { - if item.type == "Movie" { - if tvOSMovieItemCinematicView { + switch item.itemType { + case .movie: + if tvOSCinematicViews { CinematicMovieItemView(viewModel: MovieItemViewModel(item: item)) } else { MovieItemView(viewModel: MovieItemViewModel(item: item)) } - } else if item.type == "Series" { - SeriesItemView(viewModel: .init(item: item)) - } else if item.type == "Season" { - SeasonItemView(viewModel: .init(item: item)) - } else if item.type == "Episode" { - if tvOSEpisodeItemCinematicView { + case .episode: + if tvOSCinematicViews { CinematicEpisodeItemView(viewModel: EpisodeItemViewModel(item: item)) } else { EpisodeItemView(viewModel: EpisodeItemViewModel(item: item)) } - } else { + case .season: + if tvOSCinematicViews { + CinematicSeasonItemView(viewModel: SeasonItemViewModel(item: item)) + } else { + SeasonItemView(viewModel: .init(item: item)) + } + case .series: + if tvOSCinematicViews { + CinematicSeriesItemView(viewModel: SeriesItemViewModel(item: item)) + } else { + SeriesItemView(viewModel: SeriesItemViewModel(item: item)) + } + default: Text(L10n.notImplementedYetWithType(item.type ?? "")) } } diff --git a/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift b/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift index fd6d3d3c..c99fe247 100644 --- a/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift +++ b/JellyfinPlayer tvOS/Views/SettingsView/SettingsView.swift @@ -20,8 +20,7 @@ struct SettingsView: View { @Default(.videoPlayerJumpBackward) var jumpBackwardLength @Default(.downActionShowsMenu) var downActionShowsMenu @Default(.confirmClose) var confirmClose - @Default(.tvOSEpisodeItemCinematicView) var tvOSEpisodeItemCinematicView - @Default(.tvOSMovieItemCinematicView) var tvOSMovieItemCinematicView + @Default(.tvOSCinematicViews) var tvOSCinematicViews @Default(.showPosterLabels) var showPosterLabels @Default(.resumeOffset) var resumeOffset @@ -111,8 +110,7 @@ struct SettingsView: View { } Section { - Toggle("Episode Item Cinematic View", isOn: $tvOSEpisodeItemCinematicView) - Toggle("Movie Item Cinematic View", isOn: $tvOSMovieItemCinematicView) + Toggle("Cinematic Views", isOn: $tvOSCinematicViews) Toggle("Show Poster Labels", isOn: $showPosterLabels) } header: { diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 92cf26f1..4cdf637d 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -307,6 +307,9 @@ E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; }; E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; }; E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */; }; + E13F26AF278754E300DF4761 /* CinematicSeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */; }; + E13F26B12787589300DF4761 /* CinematicSeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */; }; + E13F26B32787597300DF4761 /* SingleSeasonEpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */; }; E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */; }; E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */; }; E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; @@ -650,6 +653,9 @@ E13DD3F82717E961009D4DAF /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; E13DD3FB2717EAE8009D4DAF /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; }; E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListCoordinator.swift; sourceTree = ""; }; + E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicSeriesItemView.swift; sourceTree = ""; }; + E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicSeasonItemView.swift; sourceTree = ""; }; + E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleSeasonEpisodesRowView.swift; sourceTree = ""; }; E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitMainView.swift; sourceTree = ""; }; E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeMainView.swift; sourceTree = ""; }; E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; @@ -950,11 +956,15 @@ 536D3D77267BB9650004248C /* Components */ = { isa = PBXGroup; children = ( + E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */, + E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */, + E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */, 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */, 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */, 53116A18268B947A003024C9 /* PlainLinkButton.swift */, 536D3D80267BDFC60004248C /* PortraitItemElement.swift */, + E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */, 536D3D87267C17350004248C /* PublicUserButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, ); @@ -1368,6 +1378,17 @@ path = Views; sourceTree = ""; }; + E13F26AD27874ECC00DF4761 /* CompactItemView */ = { + isa = PBXGroup; + children = ( + 53272538268C20100035FBF1 /* EpisodeItemView.swift */, + 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, + 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */, + 53116A16268B919A003024C9 /* SeriesItemView.swift */, + ); + path = CompactItemView; + sourceTree = ""; + }; E14F7D0A26DB3714007C3AE6 /* ItemView */ = { isa = PBXGroup; children = ( @@ -1445,14 +1466,8 @@ isa = PBXGroup; children = ( E1E5D53C2783A85F00692DFE /* CinematicItemView */, - 53272538268C20100035FBF1 /* EpisodeItemView.swift */, - E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */, - E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */, + E13F26AD27874ECC00DF4761 /* CompactItemView */, 53CD2A3F268A49C2002ABD4E /* ItemView.swift */, - 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */, - E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */, - 53272536268C1DBB0035FBF1 /* SeasonItemView.swift */, - 53116A16268B919A003024C9 /* SeriesItemView.swift */, ); path = ItemView; sourceTree = ""; @@ -1528,6 +1543,8 @@ E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */, E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */, E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */, + E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */, + E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */, ); path = CinematicItemView; sourceTree = ""; @@ -2049,6 +2066,7 @@ E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */, 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, + E13F26B32787597300DF4761 /* SingleSeasonEpisodesRowView.swift in Sources */, E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, @@ -2071,6 +2089,7 @@ E1E5D5402783B0C000692DFE /* CinematicItemViewTopRowButton.swift in Sources */, 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, + E13F26AF278754E300DF4761 /* CinematicSeriesItemView.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */, E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, @@ -2108,6 +2127,7 @@ C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */, E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */, E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */, + E13F26B12787589300DF4761 /* CinematicSeasonItemView.swift in Sources */, E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */, 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */, diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index d74fd68c..ecb744a0 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -67,6 +67,5 @@ extension Defaults.Keys { // tvos specific static let downActionShowsMenu = Key("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let confirmClose = Key("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite) - static let tvOSEpisodeItemCinematicView = Key("tvOSEpisodeItemCinematicView", default: false, suite: SwiftfinStore.Defaults.generalSuite) - static let tvOSMovieItemCinematicView = Key("tvOSMovieItemCinematicView", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let tvOSCinematicViews = Key("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite) } diff --git a/Shared/ViewModels/EpisodesRowViewModel.swift b/Shared/ViewModels/EpisodesRowViewModel.swift index 19874d51..68686728 100644 --- a/Shared/ViewModels/EpisodesRowViewModel.swift +++ b/Shared/ViewModels/EpisodesRowViewModel.swift @@ -12,6 +12,8 @@ import SwiftUI final class EpisodesRowViewModel: ViewModel { + // TODO: Protocol these viewmodels for generalization instead of Episode + @ObservedObject var episodeItemViewModel: EpisodeItemViewModel @Published var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:] @Published var selectedSeason: BaseItemDto? { @@ -63,3 +65,17 @@ final class EpisodesRowViewModel: ViewModel { .store(in: &cancellables) } } + +final class SingleSeasonEpisodesRowViewModel: ViewModel { + + // TODO: Protocol these viewmodels for generalization instead of Season + + @ObservedObject var seasonItemViewModel: SeasonItemViewModel + @Published var episodes: [BaseItemDto] + + init(seasonItemViewModel: SeasonItemViewModel) { + self.seasonItemViewModel = seasonItemViewModel + self.episodes = seasonItemViewModel.episodes + super.init() + } +} diff --git a/Shared/ViewModels/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel.swift index d37ec64d..85ead04a 100644 --- a/Shared/ViewModels/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel.swift @@ -15,7 +15,18 @@ import UIKit class ItemViewModel: ViewModel { @Published var item: BaseItemDto - @Published var playButtonItem: BaseItemDto? + @Published var playButtonItem: BaseItemDto? { + didSet { + playButtonItem?.createVideoPlayerViewModel() + .sink { completion in + self.handleAPIRequestError(completion: completion) + } receiveValue: { videoPlayerViewModel in + self.itemVideoPlayerViewModel = videoPlayerViewModel + self.mediaItems = videoPlayerViewModel.item.createMediaItems() + } + .store(in: &cancellables) + } + } @Published var similarItems: [BaseItemDto] = [] @Published var isWatched = false @Published var isFavorited = false diff --git a/Shared/ViewModels/SeasonItemViewModel.swift b/Shared/ViewModels/SeasonItemViewModel.swift index c5a00a40..d4a8a2dd 100644 --- a/Shared/ViewModels/SeasonItemViewModel.swift +++ b/Shared/ViewModels/SeasonItemViewModel.swift @@ -13,13 +13,15 @@ import JellyfinAPI import Stinsen final class SeasonItemViewModel: ItemViewModel { + @RouterObject var itemRouter: ItemCoordinator.Router? - @Published private(set) var episodes: [BaseItemDto] = [] + @Published var episodes: [BaseItemDto] = [] + @Published var seriesItem: BaseItemDto? override init(item: BaseItemDto) { super.init(item: item) - self.item = item + getSeriesItem() requestEpisodes() } @@ -70,7 +72,7 @@ final class SeasonItemViewModel: ItemViewModel { playButtonItem = firstEpisode } } - + func routeToSeriesItem() { guard let id = item.seriesId else { return } UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) @@ -82,4 +84,17 @@ final class SeasonItemViewModel: ItemViewModel { }) .store(in: &cancellables) } + + private func getSeriesItem() { + guard let seriesID = item.seriesId else { return } + UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, + itemId: seriesID) + .trackActivity(loading) + .sink { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + } receiveValue: { [weak self] seriesItem in + self?.seriesItem = seriesItem + } + .store(in: &cancellables) + } }