more cinematic views

This commit is contained in:
Ethan Pippin 2022-01-06 10:59:15 -07:00
parent 6ad3b3e0f2
commit 6c2d153df4
20 changed files with 419 additions and 66 deletions

View File

@ -63,5 +63,6 @@ struct PortraitItemsRowView: View {
}
.edgesIgnoringSafeArea(.horizontal)
}
.focusSection()
}
}

View File

@ -0,0 +1,122 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 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)
}
}
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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)

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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 ?? ""))
}
}

View File

@ -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: {

View File

@ -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 = "<group>"; };
E13DD3FB2717EAE8009D4DAF /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = "<group>"; };
E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListCoordinator.swift; sourceTree = "<group>"; };
E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicSeriesItemView.swift; sourceTree = "<group>"; };
E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicSeasonItemView.swift; sourceTree = "<group>"; };
E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleSeasonEpisodesRowView.swift; sourceTree = "<group>"; };
E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitMainView.swift; sourceTree = "<group>"; };
E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeMainView.swift; sourceTree = "<group>"; };
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
E13F26AD27874ECC00DF4761 /* CompactItemView */ = {
isa = PBXGroup;
children = (
53272538268C20100035FBF1 /* EpisodeItemView.swift */,
53CD2A41268A4B38002ABD4E /* MovieItemView.swift */,
53272536268C1DBB0035FBF1 /* SeasonItemView.swift */,
53116A16268B919A003024C9 /* SeriesItemView.swift */,
);
path = CompactItemView;
sourceTree = "<group>";
};
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 = "<group>";
@ -1528,6 +1543,8 @@
E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */,
E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */,
E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */,
E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */,
E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */,
);
path = CinematicItemView;
sourceTree = "<group>";
@ -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 */,

View File

@ -67,6 +67,5 @@ extension Defaults.Keys {
// tvos specific
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let tvOSEpisodeItemCinematicView = Key<Bool>("tvOSEpisodeItemCinematicView", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let tvOSMovieItemCinematicView = Key<Bool>("tvOSMovieItemCinematicView", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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)
}
}