Merge pull request #275 from LePips/tvos-home-screen-big-view

This commit is contained in:
aiden 3 2022-01-10 10:16:17 -05:00 committed by GitHub
commit 961e639970
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 742 additions and 44 deletions

View File

@ -19,6 +19,7 @@ final class ItemCoordinator: NavigationCoordinatable {
@Root var start = makeStart
@Route(.push) var item = makeItem
@Route(.push) var library = makeLibrary
@Route(.modal) var itemOverview = makeItemOverview
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
let itemDto: BaseItemDto
@ -34,6 +35,10 @@ final class ItemCoordinator: NavigationCoordinatable {
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator<ItemOverviewCoordinator> {
NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto))
}
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel))

View File

@ -0,0 +1,33 @@
//
/*
* 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 Stinsen
import SwiftUI
import JellyfinAPI
final class ItemOverviewCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ItemOverviewCoordinator.start)
@Root var start = makeStart
let item: BaseItemDto
init(item: BaseItemDto) {
self.item = item
}
@ViewBuilder func makeStart() -> some View {
#if os(tvOS)
EmptyView()
#else
ItemOverviewView(item: item)
#endif
}
}

View File

@ -20,6 +20,7 @@ extension Color {
public static let lightGray = Color(UIColor.lightGray)
#else
public static let systemFill = Color(UIColor.systemFill)
public static let systemBackground = Color(UIColor.systemBackground)
public static let secondarySystemFill = Color(UIColor.secondarySystemBackground)
public static let tertiarySystemFill = Color(UIColor.tertiarySystemBackground)
#endif

View File

@ -14,10 +14,11 @@ import JellyfinAPI
final class HomeViewModel: ViewModel {
@Published var librariesShowRecentlyAddedIDs: [String] = []
@Published var libraries: [BaseItemDto] = []
@Published var latestAddedItems: [BaseItemDto] = []
@Published var resumeItems: [BaseItemDto] = []
@Published var nextUpItems: [BaseItemDto] = []
@Published var librariesShowRecentlyAddedIDs: [String] = []
@Published var libraries: [BaseItemDto] = []
// temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
@ -59,6 +60,7 @@ final class HomeViewModel: ViewModel {
LogManager.shared.log.debug("Refresh called.")
refreshLibrariesLatest()
refreshLatestAddedItems()
refreshResumeItems()
refreshNextUpItems()
}
@ -111,13 +113,34 @@ final class HomeViewModel: ViewModel {
.store(in: &cancellables)
}
// MARK: Latest Added Items
private func refreshLatestAddedItems() {
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
enableImageTypes: [.primary, .backdrop, .thumb],
enableUserData: true,
limit: 8)
.sink { completion in
switch completion {
case .finished: ()
case .failure:
self.nextUpItems = []
self.handleAPIRequestError(completion: completion)
}
} receiveValue: { items in
LogManager.shared.log.debug("Retrieved \(String(items.count)) resume items")
self.latestAddedItems = items
}
.store(in: &cancellables)
}
// MARK: Resume Items
private func refreshResumeItems() {
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12,
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id,
limit: 6,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
mediaTypes: ["Video"],
imageTypeLimit: 1,
enableImageTypes: [.primary, .backdrop, .thumb])
enableUserData: true)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch completion {
@ -136,8 +159,10 @@ final class HomeViewModel: ViewModel {
// MARK: Next Up Items
private func refreshNextUpItems() {
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, limit: 12,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters])
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
limit: 6,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
enableUserData: true)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch completion {

View File

@ -17,14 +17,9 @@ class ItemViewModel: ViewModel {
@Published var item: 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)
if let playButtonItem = playButtonItem {
refreshItemVideoPlayerViewModel(for: playButtonItem)
}
}
}
@Published var similarItems: [BaseItemDto] = []
@ -52,6 +47,10 @@ class ItemViewModel: ViewModel {
getSimilarItems()
refreshItemVideoPlayerViewModel(for: item)
}
func refreshItemVideoPlayerViewModel(for item: BaseItemDto) {
item.createVideoPlayerViewModel()
.sink { completion in
self.handleAPIRequestError(completion: completion)

View File

@ -0,0 +1,19 @@
//
/*
* 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
extension View {
/// Applies Portrait Poster frame with proper corner radius ratio against the width
func portraitPoster(width: CGFloat) -> some View {
self.frame(width: width, height: width * 1.5)
.cornerRadius((width * 1.5) / 40)
}
}

View File

@ -0,0 +1,55 @@
//
/*
* 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 Nuke
import SwiftUI
import UIKit
class DynamicCinematicBackgroundViewModel: ObservableObject {
@Published var currentItem: BaseItemDto?
@Published var currentImageView: UIImageView?
func select(item: BaseItemDto) {
guard item.id != currentItem?.id else { return }
currentItem = item
let itemImageView = UIImageView()
let backdropImage: URL
if item.itemType == .episode {
backdropImage = item.getSeriesBackdropImage(maxWidth: 1920)
} else {
backdropImage = item.getBackdropImage(maxWidth: 1920)
}
let options = ImageLoadingOptions(transition: .fadeIn(duration: 0.2))
Nuke.loadImage(with: backdropImage, options: options, into: itemImageView, completion: { _ in })
currentImageView = itemImageView
}
}
struct CinematicBackgroundView: UIViewRepresentable {
@ObservedObject var viewModel: DynamicCinematicBackgroundViewModel
func updateUIView(_ uiView: UICinematicBackgroundView, context: Context) {
uiView.update(imageView: viewModel.currentImageView ?? UIImageView())
}
func makeUIView(context: Context) -> UICinematicBackgroundView {
return UICinematicBackgroundView(initialImageView: viewModel.currentImageView ?? UIImageView())
}
}

View File

@ -0,0 +1,62 @@
//
/*
* 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 CinematicNextUpCardView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
let item: BaseItemDto
let showOverlay: Bool
var body: some View {
VStack(alignment: .leading) {
Button {
homeRouter.route(to: \.modalItem, item)
} label: {
ZStack(alignment: .bottomLeading) {
if item.itemType == .episode {
ImageView(src: item.getSeriesBackdropImage(maxWidth: 350))
.frame(width: 350, height: 210)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 350))
.frame(width: 350, height: 210)
}
LinearGradient(colors: [.clear, .black],
startPoint: .top,
endPoint: .bottom)
.frame(height: 105)
.ignoresSafeArea()
if showOverlay {
VStack(alignment: .leading, spacing: 0) {
Text("Next")
.font(.subheadline)
.padding(.vertical, 5)
.padding(.leading, 10)
.foregroundColor(.white)
HStack {
Color.clear
.frame(width: 1, height: 7)
}
}
}
}
.frame(width: 350, height: 210)
}
.buttonStyle(CardButtonStyle())
.padding(.top)
}
.padding(.vertical)
}
}

View File

@ -0,0 +1,61 @@
//
/*
* 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 CinematicResumeCardView: 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) {
if item.itemType == .episode {
ImageView(src: item.getSeriesBackdropImage(maxWidth: 350))
.frame(width: 350, height: 210)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 350))
.frame(width: 350, height: 210)
}
LinearGradient(colors: [.clear, .black],
startPoint: .top,
endPoint: .bottom)
.frame(height: 105)
.ignoresSafeArea()
VStack(alignment: .leading, spacing: 0) {
Text(item.getItemProgressString() ?? "")
.font(.subheadline)
.padding(.vertical, 5)
.padding(.leading, 10)
.foregroundColor(.white)
HStack {
Color(UIColor.systemPurple)
.frame(width: 350 * (item.userData?.playedPercentage ?? 0) / 100, height: 7)
Spacer(minLength: 0)
}
}
}
.frame(width: 350, height: 210)
}
.buttonStyle(CardButtonStyle())
.padding(.top)
}
.padding(.vertical)
}
}

View File

@ -0,0 +1,125 @@
//
/*
* 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 UIKit
import JellyfinAPI
// TODO: Generalize this view such that it can be used in other contexts like for a library
struct HomeCinematicViewItem: Hashable {
enum TopRowType {
case resume
case nextUp
case plain
}
let item: BaseItemDto
let type: TopRowType
func hash(into hasher: inout Hasher) {
hasher.combine(item)
hasher.combine(type)
}
}
struct HomeCinematicView: View {
@FocusState var selectedItem: BaseItemDto?
@State private var updatedSelectedItem: BaseItemDto?
@State private var initiallyAppeared = false
private let forcedItemSubtitle: String?
private let items: [HomeCinematicViewItem]
private let backgroundViewModel = DynamicCinematicBackgroundViewModel()
init(items: [HomeCinematicViewItem], forcedItemSubtitle: String? = nil) {
self.items = items
self.forcedItemSubtitle = forcedItemSubtitle
}
var body: some View {
ZStack(alignment: .bottom) {
CinematicBackgroundView(viewModel: backgroundViewModel)
.frame(height: UIScreen.main.bounds.height - 10)
LinearGradient(stops: [.init(color: .clear, location: 0.5),
.init(color: .black.opacity(0.6), location: 0.7),
.init(color: .black, location: 1)],
startPoint: .top,
endPoint: .bottom)
.ignoresSafeArea()
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
if let forcedItemSubtitle = forcedItemSubtitle {
Text(forcedItemSubtitle)
.font(.callout)
.fontWeight(.medium)
.foregroundColor(Color.secondary)
} else {
if updatedSelectedItem?.itemType == .episode {
Text(updatedSelectedItem?.getEpisodeLocator() ?? "")
.font(.callout)
.fontWeight(.medium)
.foregroundColor(Color.secondary)
} else {
Text("")
}
}
Text("\(updatedSelectedItem?.seriesName ?? updatedSelectedItem?.name ?? "")")
.font(.title)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 50)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(items, id: \.self) { item in
switch item.type {
case .nextUp:
CinematicNextUpCardView(item: item.item, showOverlay: true)
.focused($selectedItem, equals: item.item)
case .resume:
CinematicResumeCardView(item: item.item)
.focused($selectedItem, equals: item.item)
case .plain:
CinematicNextUpCardView(item: item.item, showOverlay: false)
.focused($selectedItem, equals: item.item)
}
}
}
.padding(.horizontal, 50)
.padding(.bottom)
}
.focusSection()
}
}
.onChange(of: selectedItem) { newValue in
if let newItem = newValue {
backgroundViewModel.select(item: newItem)
updatedSelectedItem = newItem
}
}
.onAppear {
guard !initiallyAppeared else { return }
selectedItem = items.first?.item
updatedSelectedItem = items.first?.item
initiallyAppeared = true
}
}
}

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 SwiftUI
import UIKit
class UICinematicBackgroundView: UIView {
private var currentImageView: UIView?
private var selectDelayTimer: Timer?
init(initialImageView: UIImageView) {
super.init(frame: .zero)
initialImageView.translatesAutoresizingMaskIntoConstraints = false
initialImageView.alpha = 0
addSubview(initialImageView)
NSLayoutConstraint.activate([
initialImageView.topAnchor.constraint(equalTo: topAnchor),
initialImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
initialImageView.leftAnchor.constraint(equalTo: leftAnchor),
initialImageView.rightAnchor.constraint(equalTo: rightAnchor)
])
self.currentImageView = initialImageView
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(imageView: UIImageView) {
selectDelayTimer?.invalidate()
selectDelayTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(delayTimerTimed), userInfo: imageView, repeats: false)
}
@objc private func delayTimerTimed(timer: Timer) {
let newImageView = timer.userInfo as! UIImageView
newImageView.translatesAutoresizingMaskIntoConstraints = false
newImageView.alpha = 0
addSubview(newImageView)
NSLayoutConstraint.activate([
newImageView.topAnchor.constraint(equalTo: topAnchor),
newImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
newImageView.leftAnchor.constraint(equalTo: leftAnchor),
newImageView.rightAnchor.constraint(equalTo: rightAnchor)
])
UIView.animate(withDuration: 0.2) {
newImageView.alpha = 1
self.currentImageView?.alpha = 0
} completion: { _ in
self.currentImageView?.removeFromSuperview()
self.currentImageView = newImageView
}
}
}

View File

@ -7,13 +7,16 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Defaults
import Foundation
import SwiftUI
import JellyfinAPI
struct HomeView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
@ObservedObject var viewModel = HomeViewModel()
@Default(.showPosterLabels) var showPosterLabels
@State var showingSettings = false
@ -24,16 +27,33 @@ struct HomeView: View {
} else {
ScrollView {
LazyVStack(alignment: .leading) {
if !viewModel.resumeItems.isEmpty {
ContinueWatchingView(items: viewModel.resumeItems)
}
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)
if viewModel.resumeItems.isEmpty {
HomeCinematicView(items: viewModel.latestAddedItems.map({ .init(item: $0, type: .plain) }),
forcedItemSubtitle: "Recently Added")
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)
.focusSection()
}
} else {
HomeCinematicView(items: viewModel.resumeItems.map({ .init(item: $0, type: .resume) }))
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)
.focusSection()
}
PortraitItemsRowView(rowTitle: "Recently Added",
items: viewModel.latestAddedItems,
showItemTitles: showPosterLabels) { item in
homeRouter.route(to: \.modalItem, item)
}
}
ForEach(viewModel.libraries, id: \.self) { library in
LatestMediaView(viewModel: LatestMediaViewModel(library: library))
.focusSection()
}
Spacer(minLength: 100)
@ -52,6 +72,7 @@ struct HomeView: View {
.focusSection()
}
}
.edgesIgnoringSafeArea(.top)
.edgesIgnoringSafeArea(.horizontal)
}
}

View File

@ -17,13 +17,12 @@ struct CinematicItemAboutView: View {
var body: some View {
HStack(alignment: .top, spacing: 10) {
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 257))
.frame(width: 257, height: 380)
.cornerRadius(10)
.portraitPoster(width: 257)
ZStack(alignment: .topLeading) {
Color(UIColor.darkGray).opacity(focused ? 0.2 : 0)
.cornerRadius(30)
.frame(height: 380)
.cornerRadius(9.5)
.frame(height: 385.5)
VStack(alignment: .leading) {
Text("About")

View File

@ -246,9 +246,16 @@
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; };
C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */; };
E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */; };
E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */; };
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */; };
E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */; };
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; };
E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB952788104100354E07 /* CinematicCollectionItemView.swift */; };
E10C0941278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */; };
E10C0942278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */; };
E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */; };
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */; };
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; };
@ -423,6 +430,10 @@
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */; };
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */; };
E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */; };
E1EBCB42278BD174009FE6E9 /* TruncatedTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */; };
E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */; };
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */; };
E1EBCB4A278BE443009FE6E9 /* ItemOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */; };
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */; };
@ -638,8 +649,14 @@
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICinematicBackgroundView.swift; sourceTree = "<group>"; };
E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicBackgroundView.swift; sourceTree = "<group>"; };
E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCinematicView.swift; sourceTree = "<group>"; };
E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeCardView.swift; sourceTree = "<group>"; };
E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicNextUpCardView.swift; sourceTree = "<group>"; };
E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; };
E107BB952788104100354E07 /* CinematicCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicCollectionItemView.swift; sourceTree = "<group>"; };
E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemSize.swift; sourceTree = "<group>"; };
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = "<group>"; };
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = "<group>"; };
E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = "<group>"; };
@ -737,6 +754,9 @@
E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = "<group>"; };
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalSettingsView.swift; sourceTree = "<group>"; };
E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmCloseOverlay.swift; sourceTree = "<group>"; };
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedTextView.swift; sourceTree = "<group>"; };
E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewCoordinator.swift; sourceTree = "<group>"; };
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemOverviewView.swift; sourceTree = "<group>"; };
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallMenuOverlay.swift; sourceTree = "<group>"; };
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
@ -967,7 +987,7 @@
isa = PBXGroup;
children = (
E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */,
E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */,
E103A6A1278A7EB500820EC7 /* HomeCinematicView */,
E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */,
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */,
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */,
@ -977,6 +997,7 @@
E1E5D5412783B33900692DFE /* PortraitItemsRowView.swift */,
536D3D87267C17350004248C /* PublicUserButton.swift */,
E17885A3278105170094FBCF /* SFSymbolButton.swift */,
E13F26B22787597300DF4761 /* SingleSeasonEpisodesRowView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -1182,11 +1203,13 @@
isa = PBXGroup;
children = (
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
E1AA331C2782541500F6439C /* PrimaryButtonView.swift */,
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -1246,11 +1269,12 @@
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
E1EBCB43278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift */,
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */,
C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */,
C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */,
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
E193D5412719404B00900D82 /* MainCoordinator */,
C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */,
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
@ -1312,6 +1336,18 @@
path = Pods;
sourceTree = "<group>";
};
E103A6A1278A7EB500820EC7 /* HomeCinematicView */ = {
isa = PBXGroup;
children = (
E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */,
E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */,
E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */,
E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */,
E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */,
);
path = HomeCinematicView;
sourceTree = "<group>";
};
E107BB9127880A4000354E07 /* ItemViewModel */ = {
isa = PBXGroup;
children = (
@ -1397,18 +1433,19 @@
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
625CB56E2678C23300530A6E /* HomeView.swift */,
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
E14F7D0A26DB3714007C3AE6 /* ItemView */,
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */,
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
6213388F265F83A900A81A2A /* LibraryListView.swift */,
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
5389276F263C25230035E14B /* NextUpView.swift */,
E1E5D54A2783E26100692DFE /* SettingsView */,
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
E13DD3E427177D15009D4DAF /* ServerListView.swift */,
E1E5D54A2783E26100692DFE /* SettingsView */,
E13DD3FB2717EAE8009D4DAF /* UserListView.swift */,
E13DD3F4271793BB009D4DAF /* UserSignInView.swift */,
E193D5452719418B00900D82 /* VideoPlayer */,
@ -1430,7 +1467,6 @@
E14F7D0A26DB3714007C3AE6 /* ItemView */ = {
isa = PBXGroup;
children = (
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */,
535BAE9E2649E569005FA86D /* ItemView.swift */,
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */,
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */,
@ -1527,12 +1563,13 @@
E1AD105326D96F5A003E4A08 /* Views */ = {
isa = PBXGroup;
children = (
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
531AC8BE26750DE20091C7EB /* ImageView.swift */,
621338B22660A07800A81A2A /* LazyView.swift */,
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */,
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
E10C0940278B8DAB009DBF93 /* PortraitItemSize.swift */,
624C21742685CF60007F1390 /* SearchablePickerView.swift */,
53DE4BD1267098F300739748 /* SearchBarView.swift */,
);
@ -2039,6 +2076,7 @@
E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */,
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */,
E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */,
E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */,
C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */,
@ -2056,6 +2094,7 @@
E1E5D5462783C28100692DFE /* CinematicItemAboutView.swift in Sources */,
E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */,
E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */,
E1EBCB4A278BE443009FE6E9 /* ItemOverviewCoordinator.swift in Sources */,
53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */,
C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
536D3D88267C17350004248C /* PublicUserButton.swift in Sources */,
@ -2064,10 +2103,12 @@
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */,
E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */,
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */,
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */,
091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */,
E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */,
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */,
E1E5D5422783B33900692DFE /* PortraitItemsRowView.swift in Sources */,
@ -2098,6 +2139,7 @@
C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
E193D53727193F8700900D82 /* LibraryListCoordinator.swift in Sources */,
E10D87DF278510E400BD264C /* PosterSize.swift in Sources */,
E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */,
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */,
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */,
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
@ -2122,6 +2164,7 @@
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */,
E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */,
E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */,
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */,
5398514726B64E4100101B49 /* SearchBarView.swift in Sources */,
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
@ -2167,6 +2210,7 @@
6264E88D273850380081A12A /* Strings.swift in Sources */,
536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */,
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */,
E10C0942278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */,
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */,
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */,
@ -2207,6 +2251,7 @@
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */,
E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */,
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
@ -2237,6 +2282,7 @@
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */,
E1EBCB42278BD174009FE6E9 /* TruncatedTextView.swift in Sources */,
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
@ -2291,6 +2337,7 @@
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */,
E1C812BE277A8E5D00918266 /* VLCPlayerViewController.swift in Sources */,
E1AA331D2782541500F6439C /* PrimaryButtonView.swift in Sources */,
E10C0941278B8DAB009DBF93 /* PortraitItemSize.swift in Sources */,
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */,
@ -2308,6 +2355,7 @@
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */,
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,

View File

@ -106,6 +106,13 @@ struct EpisodesRowView: View {
bh: episode.getBackdropImageBlurHash())
.mask(Rectangle().frame(width: 200, height: 112).cornerRadius(10))
.frame(width: 200, height: 112)
.overlay {
if episode.id == viewModel.episodeItemViewModel.item.id {
RoundedRectangle(cornerRadius: 6)
.stroke(Color.jellyfinPurple, lineWidth: 4)
}
}
.padding(.top)
VStack(alignment: .leading) {
Text(episode.getEpisodeLocator() ?? "")

View File

@ -46,8 +46,7 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
ImageView(src: item.imageURLContsructor(maxWidth: Int(maxWidth)),
bh: item.blurHash,
failureInitials: item.failureInitials)
.frame(width: maxWidth, height: maxWidth * 1.5)
.cornerRadius(10)
.portraitPoster(width: maxWidth)
.shadow(radius: 4, y: 2)
if item.showTitle {

View File

@ -0,0 +1,106 @@
//
/*
* 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
struct TruncatedTextView: View {
@State private var truncated: Bool = false
@State private var shrinkText: String
private var text: String
let font: UIFont
let lineLimit: Int
let seeMoreAction: () -> Void
private var moreLessText: String {
if !truncated {
return ""
} else {
return "See More"
}
}
init(_ text: String,
lineLimit: Int,
font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body),
seeMoreAction: @escaping () -> Void) {
self.text = text
self.lineLimit = lineLimit
_shrinkText = State(wrappedValue: text)
self.font = font
self.seeMoreAction = seeMoreAction
}
var body: some View {
VStack(alignment: .center) {
Group {
Text(shrinkText)
.overlay {
if truncated {
LinearGradient(stops: [.init(color: .systemBackground.opacity(0), location: 0.5),
.init(color: .systemBackground.opacity(0.8), location: 0.7),
.init(color: .systemBackground, location: 1)],
startPoint: .top,
endPoint: .bottom)
}
}
}
.lineLimit(lineLimit)
.background {
// Render the limited text and measure its size
Text(text)
.lineLimit(lineLimit + 2)
.background {
GeometryReader { visibleTextGeometry in
Color.clear
.onAppear {
let size = CGSize(width: visibleTextGeometry.size.width, height: .greatestFiniteMagnitude)
let attributes:[NSAttributedString.Key:Any] = [NSAttributedString.Key.font: font]
var low = 0
var heigh = shrinkText.count
var mid = heigh
while ((heigh - low) > 1) {
let attributedText = NSAttributedString(string: shrinkText, attributes: attributes)
let boundingRect = attributedText.boundingRect(with: size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
if boundingRect.size.height > visibleTextGeometry.size.height {
truncated = true
heigh = mid
mid = (heigh + low)/2
} else {
if mid == text.count {
break
} else {
low = mid
mid = (low + heigh)/2
}
}
shrinkText = String(text.prefix(mid))
}
if truncated {
shrinkText = String(shrinkText.prefix(shrinkText.count - 2))
}
}
}
}
.hidden()
}
.font(Font(font))
if truncated {
Button {
seeMoreAction()
} label: {
Text(moreLessText)
}
}
}
}
}

View File

@ -53,6 +53,7 @@ struct HomeView: View {
if !viewModel.resumeItems.isEmpty {
ContinueWatchingView(viewModel: viewModel)
}
if !viewModel.nextUpItems.isEmpty {
PortraitImageHStackView(items: viewModel.nextUpItems,
horizontalAlignment: .leading) {
@ -63,7 +64,17 @@ struct HomeView: View {
} selectedAction: { item in
homeRouter.route(to: \.item, item)
}
}
if !viewModel.latestAddedItems.isEmpty {
PortraitImageHStackView(items: viewModel.latestAddedItems) {
Text("Recently Added")
.font(.title2)
.fontWeight(.bold)
.padding()
} selectedAction: { item in
homeRouter.route(to: \.item, item)
}
}
ForEach(viewModel.libraries, id: \.self) { library in

View File

@ -0,0 +1,35 @@
//
/*
* 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 ItemOverviewView: View {
@EnvironmentObject var itemOverviewRouter: ItemOverviewCoordinator.Router
let item: BaseItemDto
var body: some View {
ScrollView(showsIndicators: false) {
Text(item.overview ?? "")
.font(.footnote)
.padding()
}
.navigationBarTitle("Overview", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
itemOverviewRouter.dismissCoordinator()
} label: {
Image(systemName: "xmark.circle.fill")
}
}
}
}
}

View File

@ -13,6 +13,8 @@ import SwiftUI
struct ItemViewBody: View {
@Environment(\.horizontalSizeClass) private var hSizeClass
@Environment(\.verticalSizeClass) private var vSizeClass
@EnvironmentObject var itemRouter: ItemCoordinator.Router
@EnvironmentObject private var viewModel: ItemViewModel
@Default(.showCastAndCrew) var showCastAndCrew
@ -20,11 +22,26 @@ struct ItemViewBody: View {
var body: some View {
VStack(alignment: .leading) {
// MARK: Overview
Text(viewModel.item.overview ?? "")
.font(.footnote)
.padding(.horizontal, 16)
.padding(.vertical, 3)
if let itemOverview = viewModel.item.overview {
if hSizeClass == .compact && vSizeClass == .regular {
TruncatedTextView(itemOverview,
lineLimit: 5,
font: UIFont.preferredFont(forTextStyle: .footnote)) {
itemRouter.route(to: \.itemOverview, viewModel.item)
}
.padding(.horizontal)
.padding(.top)
} else {
Text(itemOverview)
.font(.footnote)
.padding()
}
} else {
Text("No overview available")
.font(.footnote)
.padding()
}
// MARK: Seasons

View File

@ -21,8 +21,7 @@ struct PortraitHeaderOverlayView: View {
// MARK: Portrait Image
ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130))
.frame(width: 130, height: 195)
.cornerRadius(10)
.portraitPoster(width: 130)
VStack(alignment: .leading, spacing: 1) {
Spacer()