tvos home view updates

This commit is contained in:
Ethan Pippin 2022-01-05 22:57:07 -07:00
parent feedcadfc4
commit 6ad3b3e0f2
13 changed files with 282 additions and 189 deletions

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
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)
} else {
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.isLoading {
ProgressView()
.scaleEffect(2)
} else {
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
LatestMediaView(viewModel: LatestMediaViewModel(library: library))
}
Spacer(minLength: 100)
HStack {
Spacer()
Button {
viewModel.refresh()
} label: {
Text("Refresh")
}
Spacer()
}
.focusSection()
}
}
.edgesIgnoringSafeArea(.horizontal)
}
}
}

View File

@ -13,6 +13,7 @@ import SwiftUI
struct CinematicEpisodeItemView: View {
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@ObservedObject var viewModel: EpisodeItemViewModel
@State var wrappedScrollView: UIScrollView?
@Default(.showPosterLabels) var showPosterLabels
@ -46,7 +47,9 @@ struct CinematicEpisodeItemView: 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

@ -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
@ -42,7 +43,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

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

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

@ -366,6 +366,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 */; };
@ -680,6 +682,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>"; };
@ -1295,7 +1299,7 @@
children = (
53ABFDEA2679753200886593 /* ConnectToServerView.swift */,
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */,
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */,
E1B59FD62786AE2C00A5287E /* ContinueWatchingView */,
531690E6267ABD79005D8AB9 /* HomeView.swift */,
E193D54E271942C000900D82 /* ItemView */,
536D3D7E267BDF100004248C /* LatestMediaView.swift */,
@ -1308,7 +1312,7 @@
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */,
C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */,
C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */,
531690EE267ABF72005D8AB9 /* NextUpView.swift */,
E1B59FD72786AE3E00A5287E /* NextUpView */,
E193D54F2719430400900D82 /* ServerDetailView.swift */,
E193D54A271941D300900D82 /* ServerListView.swift */,
E1E5D54D2783E66600692DFE /* SettingsView */,
@ -1481,6 +1485,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 = (
@ -1993,6 +2015,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 */,
@ -2064,6 +2087,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 */,

View File

@ -14,11 +14,11 @@ import JellyfinAPI
final class LatestMediaViewModel: ViewModel {
@Published var items = [BaseItemDto]()
let library: BaseItemDto
var libraryID: String
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,