Merge pull request #259 from LePips/tvos-view-work-and-fixes

This commit is contained in:
aiden 3 2022-01-07 09:47:53 -05:00 committed by GitHub
commit cb4a9ecba2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 713 additions and 295 deletions

View File

@ -17,11 +17,16 @@ struct PortraitItemsRowView: View {
let rowTitle: String
let items: [BaseItemDto]
let showItemTitles: Bool
let selectedAction: (BaseItemDto) -> Void
init(rowTitle: String, items: [BaseItemDto], showItemTitles: Bool = true) {
init(rowTitle: String,
items: [BaseItemDto],
showItemTitles: Bool = true,
selectedAction: @escaping (BaseItemDto) -> Void) {
self.rowTitle = rowTitle
self.items = items
self.showItemTitles = showItemTitles
self.selectedAction = selectedAction
}
var body: some View {
@ -37,7 +42,7 @@ struct PortraitItemsRowView: View {
VStack(spacing: 15) {
Button {
itemRouter.route(to: \.item, item)
selectedAction(item)
} label: {
ImageView(src: item.portraitHeaderViewURL(maxWidth: 257))
.frame(width: 257, height: 380)
@ -58,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

@ -1,46 +0,0 @@
/*
* 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 SwiftUI
import JellyfinAPI
import Combine
import Stinsen
struct ContinueWatchingView: View {
var items: [BaseItemDto]
@Namespace private var namespace
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
var body: some View {
VStack(alignment: .leading) {
if items.count > 0 {
L10n.continueWatching.text
.font(.headline)
.fontWeight(.semibold)
.padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in
Button {
self.homeRouter?.route(to: \.modalItem, item)
} label: {
LandscapeItemElement(item: item)
}
.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}
}.frame(height: 350)
} else {
EmptyView()
}
}
}
}

View File

@ -0,0 +1,71 @@
//
/*
* 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 ContinueWatchingCard: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
let item: BaseItemDto
var body: some View {
VStack(alignment: .leading) {
Button {
homeRouter.route(to: \.modalItem, item)
} label: {
ZStack(alignment: .bottom) {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
VStack(alignment: .leading, spacing: 0) {
Text(item.getItemProgressString() ?? "")
.font(.subheadline)
.padding(.vertical, 5)
.padding(.leading, 10)
.foregroundColor(.white)
HStack {
Color(UIColor.systemPurple)
.frame(width: 500 * (item.userData?.playedPercentage ?? 0) / 100, height: 13)
Spacer(minLength: 0)
}
}
.background {
LinearGradient(colors: [.clear, .black.opacity(0.5), .black.opacity(0.7)],
startPoint: .top,
endPoint: .bottom)
.ignoresSafeArea()
}
}
.frame(width: 500, height: 281.25)
}
.buttonStyle(CardButtonStyle())
.padding(.top)
VStack(alignment: .leading) {
Text("\(item.seriesName ?? item.name ?? "")")
.font(.callout)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if item.itemType == .episode {
Text(item.getEpisodeLocator() ?? "")
.font(.callout)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
}
}

View File

@ -0,0 +1,37 @@
/*
* 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 SwiftUI
import JellyfinAPI
import Combine
import Stinsen
struct ContinueWatchingView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
let items: [BaseItemDto]
var body: some View {
VStack(alignment: .leading) {
L10n.continueWatching.text
.font(.title3)
.fontWeight(.semibold)
.padding(.leading, 50)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(items, id: \.self) { item in
ContinueWatchingCard(item: item)
}
}
.padding(.horizontal, 50)
}
}
}
}

View File

@ -11,16 +11,13 @@ import Foundation
import SwiftUI
struct HomeView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel = HomeViewModel()
@ObservedObject var viewModel = HomeViewModel()
@State var showingSettings = false
var body: some View {
ZStack {
Color.black
.ignoresSafeArea()
if viewModel.isLoading {
ProgressView()
.scaleEffect(2)
@ -36,18 +33,7 @@ struct HomeView: View {
}
ForEach(viewModel.libraries, id: \.self) { library in
Button {
self.homeRouter.route(to: \.modalLibrary, (.init(parentID: library.id, filters: viewModel.recentFilterSet), title: library.name ?? ""))
} label: {
HStack {
Text(L10n.latestWithString(library.name ?? ""))
.font(.headline)
.fontWeight(.semibold)
Image(systemName: "chevron.forward.circle.fill")
}
}.padding(EdgeInsets(top: 0, leading: 90, bottom: 0, trailing: 0))
LatestMediaView(usingParentID: library.id ?? "")
LatestMediaView(viewModel: LatestMediaViewModel(library: library))
}
Spacer(minLength: 100)
@ -66,7 +52,7 @@ struct HomeView: View {
.focusSection()
}
}
}
.edgesIgnoringSafeArea(.horizontal)
}
}
}

View File

@ -13,10 +13,19 @@ import SwiftUI
struct CinematicEpisodeItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: EpisodeItemViewModel
@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 {
@ -27,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)
@ -43,10 +55,19 @@ 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,
showItemTitles: showPosterLabels)
showItemTitles: showPosterLabels) { item in
itemRouter.route(to: \.item, item)
}
}
ItemDetailsView(viewModel: viewModel)

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,37 +48,34 @@ 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 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)
@ -85,6 +88,7 @@ struct CinematicItemViewTopRow: View {
.fontWeight(.medium)
.lineLimit(1)
}
}
if let officialRating = viewModel.item.officialRating {
Text(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

@ -13,6 +13,7 @@ import SwiftUI
struct CinematicMovieItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: MovieItemViewModel
@State var wrappedScrollView: UIScrollView?
@Default(.showPosterLabels) var showPosterLabels
@ -27,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)
@ -42,7 +46,9 @@ struct CinematicMovieItemView: View {
if !viewModel.similarItems.isEmpty {
PortraitItemsRowView(rowTitle: "Recommended",
items: viewModel.similarItems,
showItemTitles: showPosterLabels)
showItemTitles: showPosterLabels) { item in
itemRouter.route(to: \.item, item)
}
}
ItemDetailsView(viewModel: viewModel)

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

@ -136,10 +136,13 @@ struct EpisodeItemView: View {
ScrollView(.horizontal) {
LazyHStack {
Spacer().frame(width: 45)
ForEach(viewModel.similarItems, id: \.id) { similarItems in
NavigationLink(destination: ItemView(item: similarItems)) {
PortraitItemElement(item: similarItems)
}.buttonStyle(PlainNavigationLinkButtonStyle())
ForEach(viewModel.similarItems, id: \.id) { similarItem in
Button {
itemRouter.route(to: \.item, similarItem)
} label: {
PortraitItemElement(item: similarItem)
}
.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}

View File

@ -11,12 +11,13 @@ import SwiftUI
import JellyfinAPI
struct MovieItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: MovieItemViewModel
@State var actors: [BaseItemPerson] = []
@State var studio: String?
@State var director: String?
@State var wrappedScrollView: UIScrollView?
@Namespace private var namespace
@ -141,10 +142,13 @@ struct MovieItemView: View {
ScrollView(.horizontal) {
LazyHStack {
Spacer().frame(width: 45)
ForEach(viewModel.similarItems, id: \.id) { similarItems in
NavigationLink(destination: ItemView(item: similarItems)) {
PortraitItemElement(item: similarItems)
}.buttonStyle(PlainNavigationLinkButtonStyle())
ForEach(viewModel.similarItems, id: \.id) { similarItem in
Button {
itemRouter.route(to: \.item, similarItem)
} label: {
PortraitItemElement(item: similarItem)
}
.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}

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

@ -5,49 +5,21 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import Defaults
import JellyfinAPI
import Combine
import SwiftUI
struct LatestMediaView: View {
@StateObject var tempViewModel = ViewModel()
@State var items: [BaseItemDto] = []
@State private var viewDidLoad: Bool = false
private var library_id: String = ""
init(usingParentID: String) {
library_id = usingParentID
}
func onAppear() {
if viewDidLoad == true {
return
}
viewDidLoad = true
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], enableUserData: true, limit: 12)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
items = response
})
.store(in: &tempViewModel.cancellables)
}
@EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel: LatestMediaViewModel
@Default(.showPosterLabels) var showPosterLabels
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in
NavigationLink(destination: LazyView { ItemView(item: item) }) {
PortraitItemElement(item: item)
}.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}
}.frame(height: 480)
.onAppear(perform: onAppear)
PortraitItemsRowView(rowTitle: L10n.latestWithString(viewModel.library.name ?? ""),
items: viewModel.items,
showItemTitles: showPosterLabels) { item in
homeRouter.route(to: \.modalItem, item)
}
}
}

View File

@ -1,45 +0,0 @@
/*
* 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 SwiftUI
import JellyfinAPI
import Combine
import Stinsen
struct NextUpView: View {
var items: [BaseItemDto]
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
var body: some View {
VStack(alignment: .leading) {
if items.count > 0 {
L10n.nextUp.text
.font(.headline)
.fontWeight(.semibold)
.padding(.leading, 90)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
Spacer().frame(width: 45)
ForEach(items, id: \.id) { item in
Button {
self.homeRouter?.route(to: \.modalItem, item)
} label: {
LandscapeItemElement(item: item)
}.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}
}.frame(height: 350)
.offset(y: -10)
} else {
EmptyView()
}
}
}
}

View File

@ -0,0 +1,46 @@
//
/*
* 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 NextUpCard: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
let item: BaseItemDto
var body: some View {
VStack(alignment: .leading) {
Button {
homeRouter.route(to: \.modalItem, item)
} label: {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
}
.buttonStyle(CardButtonStyle())
.padding(.top)
VStack(alignment: .leading) {
Text("\(item.seriesName ?? item.name ?? "")")
.font(.callout)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if item.itemType == .episode {
Text(item.getEpisodeLocator() ?? "")
.font(.callout)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
}
}

View File

@ -0,0 +1,37 @@
/*
* 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 SwiftUI
import JellyfinAPI
import Combine
import Stinsen
struct NextUpView: View {
var items: [BaseItemDto]
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
var body: some View {
VStack(alignment: .leading) {
L10n.nextUp.text
.font(.title3)
.fontWeight(.semibold)
.padding(.leading, 50)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(items, id: \.id) { item in
NextUpCard(item: item)
}
}
.padding(.horizontal, 50)
}
}
}
}

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 */; };
@ -366,6 +369,8 @@
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; };
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; };
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; };
E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */; };
E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD82786AE4600A5287E /* NextUpCard.swift */; };
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; };
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE9271A23880015B715 /* SwiftyJSON */; };
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; };
@ -648,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>"; };
@ -680,6 +688,8 @@
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillHStackView.swift; sourceTree = "<group>"; };
E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGUIDPairExtensions.swift; sourceTree = "<group>"; };
E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitHeaderOverlayView.swift; sourceTree = "<group>"; };
E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = "<group>"; };
E1B59FD82786AE4600A5287E /* NextUpCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpCard.swift; sourceTree = "<group>"; };
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = "<group>"; };
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = "<group>"; };
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
@ -946,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 */,
);
@ -1295,7 +1309,7 @@
children = (
53ABFDEA2679753200886593 /* ConnectToServerView.swift */,
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */,
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */,
E1B59FD62786AE2C00A5287E /* ContinueWatchingView */,
531690E6267ABD79005D8AB9 /* HomeView.swift */,
E193D54E271942C000900D82 /* ItemView */,
536D3D7E267BDF100004248C /* LatestMediaView.swift */,
@ -1308,7 +1322,7 @@
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */,
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */,
C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */,
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
E1B59FD72786AE3E00A5287E /* NextUpView */,
E193D54F2719430400900D82 /* ServerDetailView.swift */,
E193D54A271941D300900D82 /* ServerListView.swift */,
E1E5D54D2783E66600692DFE /* SettingsView */,
@ -1364,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 = (
@ -1441,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>";
@ -1481,6 +1500,24 @@
path = Views;
sourceTree = "<group>";
};
E1B59FD62786AE2C00A5287E /* ContinueWatchingView */ = {
isa = PBXGroup;
children = (
E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */,
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */,
);
path = ContinueWatchingView;
sourceTree = "<group>";
};
E1B59FD72786AE3E00A5287E /* NextUpView */ = {
isa = PBXGroup;
children = (
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
E1B59FD82786AE4600A5287E /* NextUpCard.swift */,
);
path = NextUpView;
sourceTree = "<group>";
};
E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = {
isa = PBXGroup;
children = (
@ -1506,6 +1543,8 @@
E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */,
E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */,
E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */,
E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */,
E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */,
);
path = CinematicItemView;
sourceTree = "<group>";
@ -1993,6 +2032,7 @@
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */,
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */,
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */,
@ -2026,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 */,
@ -2048,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 */,
@ -2064,6 +2106,7 @@
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */,
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */,
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */,
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */,
@ -2084,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

@ -68,7 +68,7 @@ struct HomeView: View {
ForEach(viewModel.libraries, id: \.self) { library in
LatestMediaView(viewModel: LatestMediaViewModel(libraryID: library.id!)) {
LatestMediaView(viewModel: LatestMediaViewModel(library: library)) {
HStack {
Text(L10n.latestWithString(library.name ?? ""))
.font(.title2)

View File

@ -51,24 +51,6 @@ private struct ItemView: View {
}
}
@ViewBuilder
var toolbarItemContent: some View {
switch viewModel.item.itemType {
case .season:
Menu {
Button {
(viewModel as? SeasonItemViewModel)?.routeToSeriesItem()
} label: {
Label("Show Series", systemImage: "text.below.photo")
}
} label: {
Image(systemName: "ellipsis.circle.fill")
}
default:
EmptyView()
}
}
var body: some View {
Group {
if hSizeClass == .compact && vSizeClass == .regular {
@ -79,10 +61,5 @@ private struct ItemView: View {
.environmentObject(viewModel)
}
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
toolbarItemContent
}
}
}
}

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

@ -15,10 +15,10 @@ final class LatestMediaViewModel: ViewModel {
@Published var items = [BaseItemDto]()
var libraryID: String
let library: BaseItemDto
init(libraryID: String) {
self.libraryID = libraryID
init(library: BaseItemDto) {
self.library = library
super.init()
requestLatestMedia()
@ -27,7 +27,7 @@ final class LatestMediaViewModel: ViewModel {
func requestLatestMedia() {
LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)")
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
parentId: libraryID,
parentId: library.id ?? "",
fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,

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()
}
@ -71,15 +73,16 @@ final class SeasonItemViewModel: ItemViewModel {
}
}
func routeToSeriesItem() {
guard let id = item.seriesId else { return }
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
private func getSeriesItem() {
guard let seriesID = item.seriesId else { return }
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id,
itemId: seriesID)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
.sink { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] item in
self?.itemRouter?.route(to: \.item, item)
})
} receiveValue: { [weak self] seriesItem in
self?.seriesItem = seriesItem
}
.store(in: &cancellables)
}
}