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 rowTitle: String
let items: [BaseItemDto] let items: [BaseItemDto]
let showItemTitles: Bool 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.rowTitle = rowTitle
self.items = items self.items = items
self.showItemTitles = showItemTitles self.showItemTitles = showItemTitles
self.selectedAction = selectedAction
} }
var body: some View { var body: some View {
@ -37,7 +42,7 @@ struct PortraitItemsRowView: View {
VStack(spacing: 15) { VStack(spacing: 15) {
Button { Button {
itemRouter.route(to: \.item, item) selectedAction(item)
} label: { } label: {
ImageView(src: item.portraitHeaderViewURL(maxWidth: 257)) ImageView(src: item.portraitHeaderViewURL(maxWidth: 257))
.frame(width: 257, height: 380) .frame(width: 257, height: 380)
@ -58,5 +63,6 @@ struct PortraitItemsRowView: View {
} }
.edgesIgnoringSafeArea(.horizontal) .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,62 +11,48 @@ import Foundation
import SwiftUI import SwiftUI
struct HomeView: View { struct HomeView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router @EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel = HomeViewModel() @ObservedObject var viewModel = HomeViewModel()
@State var showingSettings = false @State var showingSettings = false
var body: some View { var body: some View {
ZStack { if viewModel.isLoading {
Color.black ProgressView()
.ignoresSafeArea() .scaleEffect(2)
} else {
if viewModel.isLoading { ScrollView {
ProgressView() LazyVStack(alignment: .leading) {
.scaleEffect(2) if !viewModel.resumeItems.isEmpty {
} else { ContinueWatchingView(items: viewModel.resumeItems)
ScrollView {
LazyVStack(alignment: .leading) {
if !viewModel.resumeItems.isEmpty {
ContinueWatchingView(items: viewModel.resumeItems)
}
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)
}
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 ?? "")
}
Spacer(minLength: 100)
HStack {
Spacer()
Button {
viewModel.refresh()
} label: {
Text("Refresh")
}
Spacer()
}
.focusSection()
} }
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)
}
ForEach(viewModel.libraries, id: \.self) { library in
LatestMediaView(viewModel: LatestMediaViewModel(library: library))
}
Spacer(minLength: 100)
HStack {
Spacer()
Button {
viewModel.refresh()
} label: {
Text("Refresh")
}
Spacer()
}
.focusSection()
} }
} }
.edgesIgnoringSafeArea(.horizontal)
} }
} }
} }

View File

@ -13,10 +13,19 @@ import SwiftUI
struct CinematicEpisodeItemView: View { struct CinematicEpisodeItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: EpisodeItemViewModel @ObservedObject var viewModel: EpisodeItemViewModel
@State var wrappedScrollView: UIScrollView? @State var wrappedScrollView: UIScrollView?
@Default(.showPosterLabels) var showPosterLabels @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 { var body: some View {
ZStack { ZStack {
@ -27,7 +36,10 @@ struct CinematicEpisodeItemView: View {
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView) CinematicItemViewTopRow(viewModel: viewModel,
wrappedScrollView: wrappedScrollView,
title: viewModel.item.name ?? "",
subtitle: generateSubtitle())
.focusSection() .focusSection()
.frame(height: UIScreen.main.bounds.height - 10) .frame(height: UIScreen.main.bounds.height - 10)
@ -43,10 +55,19 @@ struct CinematicEpisodeItemView: View {
EpisodesRowView(viewModel: EpisodesRowViewModel(episodeItemViewModel: viewModel)) EpisodesRowView(viewModel: EpisodesRowViewModel(episodeItemViewModel: viewModel))
.focusSection() .focusSection()
if let seriesItem = viewModel.series {
PortraitItemsRowView(rowTitle: "Series",
items: [seriesItem]) { seriesItem in
itemRouter.route(to: \.item, seriesItem)
}
}
if !viewModel.similarItems.isEmpty { if !viewModel.similarItems.isEmpty {
PortraitItemsRowView(rowTitle: "Recommended", PortraitItemsRowView(rowTitle: "Recommended",
items: viewModel.similarItems, items: viewModel.similarItems,
showItemTitles: showPosterLabels) showItemTitles: showPosterLabels) { item in
itemRouter.route(to: \.item, item)
}
} }
ItemDetailsView(viewModel: viewModel) ItemDetailsView(viewModel: viewModel)

View File

@ -16,6 +16,8 @@ struct CinematicItemViewTopRow: View {
@Environment(\.isFocused) var envFocused: Bool @Environment(\.isFocused) var envFocused: Bool
@State var focused: Bool = false @State var focused: Bool = false
@State var wrappedScrollView: UIScrollView? @State var wrappedScrollView: UIScrollView?
@State var title: String
@State var subtitle: String?
var body: some View { var body: some View {
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
@ -34,7 +36,11 @@ struct CinematicItemViewTopRow: View {
// MARK: Play // MARK: Play
Button { 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: { } label: {
HStack(spacing: 15) { HStack(spacing: 15) {
Image(systemName: "play.fill") Image(systemName: "play.fill")
@ -42,48 +48,46 @@ struct CinematicItemViewTopRow: View {
.font(.title3) .font(.title3)
Text(viewModel.playButtonText()) Text(viewModel.playButtonText())
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black) .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.black)
// .font(.title3)
.fontWeight(.semibold) .fontWeight(.semibold)
} }
.frame(width: 230, height: 100) .frame(width: 230, height: 100)
.background(viewModel.playButtonItem == nil ? Color.secondarySystemFill : Color.white) .background(viewModel.playButtonItem == nil ? Color.secondarySystemFill : Color.white)
.cornerRadius(10) .cornerRadius(10)
// ZStack {
// Color.white.frame(width: 230, height: 100)
//
// Text("Play")
// .font(.title3)
// .foregroundColor(.black)
// }
} }
.buttonStyle(CardButtonStyle()) .buttonStyle(CardButtonStyle())
.disabled(viewModel.itemVideoPlayerViewModel == nil)
} }
} }
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text(viewModel.item.name ?? "") Text(title)
.font(.title2) .font(.title2)
.lineLimit(2) .lineLimit(2)
if let seriesName = viewModel.item.seriesName, let episodeLocator = viewModel.item.getEpisodeLocator() { if let subtitle = subtitle {
Text("\(seriesName) - \(episodeLocator)") Text(subtitle)
} }
HStack(alignment: .PlayInformationAlignmentGuide, spacing: 20) { HStack(alignment: .PlayInformationAlignmentGuide, spacing: 20) {
if let runtime = viewModel.item.getItemRuntime() { if viewModel.item.itemType == .series {
Text(runtime) if let airTime = viewModel.item.airTime {
.font(.subheadline) Text(airTime)
.fontWeight(.medium) .font(.subheadline)
} .fontWeight(.medium)
}
if let productionYear = viewModel.item.productionYear { } else {
Text(String(productionYear)) if let runtime = viewModel.item.getItemRuntime() {
.font(.subheadline) Text(runtime)
.fontWeight(.medium) .font(.subheadline)
.lineLimit(1) .fontWeight(.medium)
}
if let productionYear = viewModel.item.productionYear {
Text(String(productionYear))
.font(.subheadline)
.fontWeight(.medium)
.lineLimit(1)
}
} }
if let officialRating = viewModel.item.officialRating { 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 { extension VerticalAlignment {
private struct PlayInformationAlignment: AlignmentID { private struct PlayInformationAlignment: AlignmentID {

View File

@ -13,6 +13,7 @@ import SwiftUI
struct CinematicMovieItemView: View { struct CinematicMovieItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: MovieItemViewModel @ObservedObject var viewModel: MovieItemViewModel
@State var wrappedScrollView: UIScrollView? @State var wrappedScrollView: UIScrollView?
@Default(.showPosterLabels) var showPosterLabels @Default(.showPosterLabels) var showPosterLabels
@ -27,7 +28,10 @@ struct CinematicMovieItemView: View {
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
CinematicItemViewTopRow(viewModel: viewModel, wrappedScrollView: wrappedScrollView) CinematicItemViewTopRow(viewModel: viewModel,
wrappedScrollView: wrappedScrollView,
title: viewModel.item.name ?? "",
subtitle: nil)
.focusSection() .focusSection()
.frame(height: UIScreen.main.bounds.height - 10) .frame(height: UIScreen.main.bounds.height - 10)
@ -42,7 +46,9 @@ struct CinematicMovieItemView: View {
if !viewModel.similarItems.isEmpty { if !viewModel.similarItems.isEmpty {
PortraitItemsRowView(rowTitle: "Recommended", PortraitItemsRowView(rowTitle: "Recommended",
items: viewModel.similarItems, items: viewModel.similarItems,
showItemTitles: showPosterLabels) showItemTitles: showPosterLabels) { item in
itemRouter.route(to: \.item, item)
}
} }
ItemDetailsView(viewModel: viewModel) 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) { ScrollView(.horizontal) {
LazyHStack { LazyHStack {
Spacer().frame(width: 45) Spacer().frame(width: 45)
ForEach(viewModel.similarItems, id: \.id) { similarItems in ForEach(viewModel.similarItems, id: \.id) { similarItem in
NavigationLink(destination: ItemView(item: similarItems)) { Button {
PortraitItemElement(item: similarItems) itemRouter.route(to: \.item, similarItem)
}.buttonStyle(PlainNavigationLinkButtonStyle()) } label: {
PortraitItemElement(item: similarItem)
}
.buttonStyle(PlainNavigationLinkButtonStyle())
} }
Spacer().frame(width: 45) Spacer().frame(width: 45)
} }

View File

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

View File

@ -25,8 +25,7 @@ struct ItemNavigationView: View {
struct ItemView: View { struct ItemView: View {
@Default(.tvOSEpisodeItemCinematicView) var tvOSEpisodeItemCinematicView @Default(.tvOSCinematicViews) var tvOSCinematicViews
@Default(.tvOSMovieItemCinematicView) var tvOSMovieItemCinematicView
private var item: BaseItemDto private var item: BaseItemDto
@ -36,23 +35,32 @@ struct ItemView: View {
var body: some View { var body: some View {
Group { Group {
if item.type == "Movie" { switch item.itemType {
if tvOSMovieItemCinematicView { case .movie:
if tvOSCinematicViews {
CinematicMovieItemView(viewModel: MovieItemViewModel(item: item)) CinematicMovieItemView(viewModel: MovieItemViewModel(item: item))
} else { } else {
MovieItemView(viewModel: MovieItemViewModel(item: item)) MovieItemView(viewModel: MovieItemViewModel(item: item))
} }
} else if item.type == "Series" { case .episode:
SeriesItemView(viewModel: .init(item: item)) if tvOSCinematicViews {
} else if item.type == "Season" {
SeasonItemView(viewModel: .init(item: item))
} else if item.type == "Episode" {
if tvOSEpisodeItemCinematicView {
CinematicEpisodeItemView(viewModel: EpisodeItemViewModel(item: item)) CinematicEpisodeItemView(viewModel: EpisodeItemViewModel(item: item))
} else { } else {
EpisodeItemView(viewModel: EpisodeItemViewModel(item: item)) 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 ?? "")) Text(L10n.notImplementedYetWithType(item.type ?? ""))
} }
} }

View File

@ -5,49 +5,21 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors * Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/ */
import SwiftUI import Defaults
import JellyfinAPI import JellyfinAPI
import Combine import SwiftUI
struct LatestMediaView: View { struct LatestMediaView: View {
@StateObject var tempViewModel = ViewModel()
@State var items: [BaseItemDto] = []
@State private var viewDidLoad: Bool = false
private var library_id: String = "" @EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel: LatestMediaViewModel
init(usingParentID: String) { @Default(.showPosterLabels) var showPosterLabels
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)
}
var body: some View { var body: some View {
ScrollView(.horizontal, showsIndicators: false) { PortraitItemsRowView(rowTitle: L10n.latestWithString(viewModel.library.name ?? ""),
LazyHStack { items: viewModel.items,
Spacer().frame(width: 45) showItemTitles: showPosterLabels) { item in
ForEach(items, id: \.id) { item in homeRouter.route(to: \.modalItem, item)
NavigationLink(destination: LazyView { ItemView(item: item) }) { }
PortraitItemElement(item: item)
}.buttonStyle(PlainNavigationLinkButtonStyle())
}
Spacer().frame(width: 45)
}
}.frame(height: 480)
.onAppear(perform: onAppear)
} }
} }

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(.videoPlayerJumpBackward) var jumpBackwardLength
@Default(.downActionShowsMenu) var downActionShowsMenu @Default(.downActionShowsMenu) var downActionShowsMenu
@Default(.confirmClose) var confirmClose @Default(.confirmClose) var confirmClose
@Default(.tvOSEpisodeItemCinematicView) var tvOSEpisodeItemCinematicView @Default(.tvOSCinematicViews) var tvOSCinematicViews
@Default(.tvOSMovieItemCinematicView) var tvOSMovieItemCinematicView
@Default(.showPosterLabels) var showPosterLabels @Default(.showPosterLabels) var showPosterLabels
@Default(.resumeOffset) var resumeOffset @Default(.resumeOffset) var resumeOffset
@ -111,8 +110,7 @@ struct SettingsView: View {
} }
Section { Section {
Toggle("Episode Item Cinematic View", isOn: $tvOSEpisodeItemCinematicView) Toggle("Cinematic Views", isOn: $tvOSCinematicViews)
Toggle("Movie Item Cinematic View", isOn: $tvOSMovieItemCinematicView)
Toggle("Show Poster Labels", isOn: $showPosterLabels) Toggle("Show Poster Labels", isOn: $showPosterLabels)
} header: { } header: {

View File

@ -307,6 +307,9 @@
E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; }; E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3F82717E961009D4DAF /* UserListViewModel.swift */; };
E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; }; E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3FB2717EAE8009D4DAF /* UserListView.swift */; };
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD4012717EE79009D4DAF /* UserListCoordinator.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 */; }; E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */; };
E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */; }; E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */; };
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.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 */; }; E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; };
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; };
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.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 */; }; E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; };
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE9271A23880015B715 /* SwiftyJSON */; }; E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE9271A23880015B715 /* SwiftyJSON */; };
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
@ -946,11 +956,15 @@
536D3D77267BB9650004248C /* Components */ = { 536D3D77267BB9650004248C /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */,
E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */,
E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */,
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */, E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */,
53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */, 53272531268BF09D0035FBF1 /* MediaViewActionButton.swift */,
53116A18268B947A003024C9 /* PlainLinkButton.swift */, 53116A18268B947A003024C9 /* PlainLinkButton.swift */,
536D3D80267BDFC60004248C /* PortraitItemElement.swift */, 536D3D80267BDFC60004248C /* PortraitItemElement.swift */,
E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */,
536D3D87267C17350004248C /* PublicUserButton.swift */, 536D3D87267C17350004248C /* PublicUserButton.swift */,
E17885A3278105170094FBCF /* SFSymbolButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */,
); );
@ -1295,7 +1309,7 @@
children = ( children = (
53ABFDEA2679753200886593 /* ConnectToServerView.swift */, 53ABFDEA2679753200886593 /* ConnectToServerView.swift */,
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */, E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */,
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */, E1B59FD62786AE2C00A5287E /* ContinueWatchingView */,
531690E6267ABD79005D8AB9 /* HomeView.swift */, 531690E6267ABD79005D8AB9 /* HomeView.swift */,
E193D54E271942C000900D82 /* ItemView */, E193D54E271942C000900D82 /* ItemView */,
536D3D7E267BDF100004248C /* LatestMediaView.swift */, 536D3D7E267BDF100004248C /* LatestMediaView.swift */,
@ -1308,7 +1322,7 @@
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */, C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */,
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */,
C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */, C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */,
531690EE267ABF72005D8AB9 /* NextUpView.swift */, E1B59FD72786AE3E00A5287E /* NextUpView */,
E193D54F2719430400900D82 /* ServerDetailView.swift */, E193D54F2719430400900D82 /* ServerDetailView.swift */,
E193D54A271941D300900D82 /* ServerListView.swift */, E193D54A271941D300900D82 /* ServerListView.swift */,
E1E5D54D2783E66600692DFE /* SettingsView */, E1E5D54D2783E66600692DFE /* SettingsView */,
@ -1364,6 +1378,17 @@
path = Views; path = Views;
sourceTree = "<group>"; 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 */ = { E14F7D0A26DB3714007C3AE6 /* ItemView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1441,14 +1466,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1E5D53C2783A85F00692DFE /* CinematicItemView */, E1E5D53C2783A85F00692DFE /* CinematicItemView */,
53272538268C20100035FBF1 /* EpisodeItemView.swift */, E13F26AD27874ECC00DF4761 /* CompactItemView */,
E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */,
E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */,
53CD2A3F268A49C2002ABD4E /* ItemView.swift */, 53CD2A3F268A49C2002ABD4E /* ItemView.swift */,
53CD2A41268A4B38002ABD4E /* MovieItemView.swift */,
E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */,
53272536268C1DBB0035FBF1 /* SeasonItemView.swift */,
53116A16268B919A003024C9 /* SeriesItemView.swift */,
); );
path = ItemView; path = ItemView;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1481,6 +1500,24 @@
path = Views; path = Views;
sourceTree = "<group>"; 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 */ = { E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1506,6 +1543,8 @@
E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */, E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */,
E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */, E1E5D53F2783B0C000692DFE /* CinematicItemViewTopRowButton.swift */,
E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */, E1E5D53D2783B05200692DFE /* CinematicMovieItemView.swift */,
E13F26B02787589300DF4761 /* CinematicSeasonItemView.swift */,
E13F26AE278754E300DF4761 /* CinematicSeriesItemView.swift */,
); );
path = CinematicItemView; path = CinematicItemView;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1993,6 +2032,7 @@
531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */,
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */, E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */,
E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */,
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */, E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */,
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */, C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */,
@ -2026,6 +2066,7 @@
E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */, E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */,
53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */,
531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */,
E13F26B32787597300DF4761 /* SingleSeasonEpisodesRowView.swift in Sources */,
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
@ -2048,6 +2089,7 @@
E1E5D5402783B0C000692DFE /* CinematicItemViewTopRowButton.swift in Sources */, E1E5D5402783B0C000692DFE /* CinematicItemViewTopRowButton.swift in Sources */,
5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */, 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */,
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
E13F26AF278754E300DF4761 /* CinematicSeriesItemView.swift in Sources */,
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */, 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */,
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */,
@ -2064,6 +2106,7 @@
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */, E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */,
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */,
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */, E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */,
E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */, E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */,
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */, E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */,
@ -2084,6 +2127,7 @@
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */, C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */,
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */, E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */, E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */,
E13F26B12787589300DF4761 /* CinematicSeasonItemView.swift in Sources */,
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */, E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */,
E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */, E10D87E327852FD000BD264C /* EpisodesRowViewModel.swift in Sources */,

View File

@ -68,7 +68,7 @@ struct HomeView: View {
ForEach(viewModel.libraries, id: \.self) { library in ForEach(viewModel.libraries, id: \.self) { library in
LatestMediaView(viewModel: LatestMediaViewModel(libraryID: library.id!)) { LatestMediaView(viewModel: LatestMediaViewModel(library: library)) {
HStack { HStack {
Text(L10n.latestWithString(library.name ?? "")) Text(L10n.latestWithString(library.name ?? ""))
.font(.title2) .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 { var body: some View {
Group { Group {
if hSizeClass == .compact && vSizeClass == .regular { if hSizeClass == .compact && vSizeClass == .regular {
@ -79,10 +61,5 @@ private struct ItemView: View {
.environmentObject(viewModel) .environmentObject(viewModel)
} }
} }
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
toolbarItemContent
}
}
} }
} }

View File

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

View File

@ -12,6 +12,8 @@ import SwiftUI
final class EpisodesRowViewModel: ViewModel { final class EpisodesRowViewModel: ViewModel {
// TODO: Protocol these viewmodels for generalization instead of Episode
@ObservedObject var episodeItemViewModel: EpisodeItemViewModel @ObservedObject var episodeItemViewModel: EpisodeItemViewModel
@Published var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:] @Published var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
@Published var selectedSeason: BaseItemDto? { @Published var selectedSeason: BaseItemDto? {
@ -63,3 +65,17 @@ final class EpisodesRowViewModel: ViewModel {
.store(in: &cancellables) .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 { class ItemViewModel: ViewModel {
@Published var item: BaseItemDto @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 similarItems: [BaseItemDto] = []
@Published var isWatched = false @Published var isWatched = false
@Published var isFavorited = false @Published var isFavorited = false

View File

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

View File

@ -13,13 +13,15 @@ import JellyfinAPI
import Stinsen import Stinsen
final class SeasonItemViewModel: ItemViewModel { final class SeasonItemViewModel: ItemViewModel {
@RouterObject var itemRouter: ItemCoordinator.Router? @RouterObject var itemRouter: ItemCoordinator.Router?
@Published private(set) var episodes: [BaseItemDto] = [] @Published var episodes: [BaseItemDto] = []
@Published var seriesItem: BaseItemDto?
override init(item: BaseItemDto) { override init(item: BaseItemDto) {
super.init(item: item) super.init(item: item)
self.item = item
getSeriesItem()
requestEpisodes() requestEpisodes()
} }
@ -70,16 +72,17 @@ final class SeasonItemViewModel: ItemViewModel {
playButtonItem = firstEpisode playButtonItem = firstEpisode
} }
} }
func routeToSeriesItem() { private func getSeriesItem() {
guard let id = item.seriesId else { return } guard let seriesID = item.seriesId else { return }
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id) UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id,
itemId: seriesID)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] item in } receiveValue: { [weak self] seriesItem in
self?.itemRouter?.route(to: \.item, item) self?.seriesItem = seriesItem
}) }
.store(in: &cancellables) .store(in: &cancellables)
} }
} }