From 3c81c7532f53186bba692b87f9aea1ec992dc2a7 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Fri, 9 Sep 2022 19:11:09 -0600 Subject: [PATCH] tvOS - Refactor Cinematic Item Selector (#564) --- .../BaseItemDto+Images.swift | 8 +- .../BaseItemDtoExtensions.swift | 3 +- Shared/Objects/PosterType.swift | 4 +- Shared/Views/ProgressBar.swift | 1 - .../Components/CinematicItemSelector.swift | 245 ++++++++++++++++++ .../CinematicBackgroundView.swift | 59 ----- .../CinematicNextUpCardView.swift | 70 ----- .../CinematicResumeCardView.swift | 78 ------ .../HomeCinematicView/HomeCinematicView.swift | 134 ---------- .../UICinematicBackgroundView.swift | 76 ------ .../LandscapePosterProgressBar.swift | 40 +++ Swiftfin tvOS/Components/PosterButton.swift | 110 +++----- Swiftfin tvOS/Components/PosterHStack.swift | 119 ++++----- .../ContinueWatchingCard.swift | 85 ------ .../ContinueWatchingView.swift | 38 --- Swiftfin tvOS/Views/HomeView.swift | 73 ------ .../Views/HomeView/HomeContentView.swift | 131 ++++++++++ .../Views/HomeView/HomeErrorView.swift | 53 ++++ Swiftfin tvOS/Views/HomeView/HomeView.swift | 35 +++ Swiftfin tvOS/Views/LatestInLibraryView.swift | 2 +- Swiftfin tvOS/Views/LibraryView.swift | 2 +- Swiftfin tvOS/Views/MediaView.swift | 2 +- Swiftfin.xcodeproj/project.pbxproj | 76 ++---- .../LandscapePosterProgressBar.swift | 1 + 24 files changed, 631 insertions(+), 814 deletions(-) create mode 100644 Swiftfin tvOS/Components/CinematicItemSelector.swift delete mode 100644 Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift delete mode 100644 Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift delete mode 100644 Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift delete mode 100644 Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift delete mode 100644 Swiftfin tvOS/Components/HomeCinematicView/UICinematicBackgroundView.swift create mode 100644 Swiftfin tvOS/Components/LandscapePosterProgressBar.swift delete mode 100644 Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift delete mode 100644 Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift delete mode 100644 Swiftfin tvOS/Views/HomeView.swift create mode 100644 Swiftfin tvOS/Views/HomeView/HomeContentView.swift create mode 100644 Swiftfin tvOS/Views/HomeView/HomeErrorView.swift create mode 100644 Swiftfin tvOS/Views/HomeView/HomeView.swift rename {Shared/Views => Swiftfin/Components}/LandscapePosterProgressBar.swift (97%) diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift index 0c75677b..b11814ee 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Images.swift @@ -56,9 +56,7 @@ extension BaseItemDto { } func seriesImageURL(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> URL { - let maxWidth = maxWidth != nil ? Int(maxWidth!) : nil - let maxHeight = maxHeight != nil ? Int(maxHeight!) : nil - return _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesId ?? "") + _imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: seriesId ?? "") } func seriesImageSource(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> ImageSource { @@ -66,6 +64,10 @@ extension BaseItemDto { return ImageSource(url: url, blurHash: nil) } + func seriesImageSource(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> ImageSource { + seriesImageSource(type, maxWidth: Int(maxWidth), maxHeight: Int(maxWidth)) + } + func seriesImageSource(_ type: ImageType, maxWidth: CGFloat) -> ImageSource { seriesImageSource(type, maxWidth: Int(maxWidth)) } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 6e0a6dba..91adba78 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -11,6 +11,7 @@ import JellyfinAPI import UIKit extension BaseItemDto: Identifiable {} +extension BaseItemDto: LibraryParent {} extension BaseItemDto { @@ -247,5 +248,3 @@ extension BaseItemDtoImageBlurHashes { } } } - -extension BaseItemDto: LibraryParent {} diff --git a/Shared/Objects/PosterType.swift b/Shared/Objects/PosterType.swift index 84350fbb..e60a7921 100644 --- a/Shared/Objects/PosterType.swift +++ b/Shared/Objects/PosterType.swift @@ -34,9 +34,9 @@ enum PosterType: String, CaseIterable, Defaults.Serializable { enum Width { #if os(tvOS) - static let portrait = 250.0 + static let portrait = 200.0 - static let landscape = 490.0 + static let landscape = 350.0 #else @ScaledMetric(relativeTo: .largeTitle) static var portrait = 100.0 diff --git a/Shared/Views/ProgressBar.swift b/Shared/Views/ProgressBar.swift index aa138082..3709865e 100644 --- a/Shared/Views/ProgressBar.swift +++ b/Shared/Views/ProgressBar.swift @@ -23,6 +23,5 @@ struct ProgressBar: View { .scaleEffect(x: progress, y: 1, anchor: .leading) } .frame(maxWidth: .infinity) - .frame(height: 3) } } diff --git a/Swiftfin tvOS/Components/CinematicItemSelector.swift b/Swiftfin tvOS/Components/CinematicItemSelector.swift new file mode 100644 index 00000000..8cb2b71f --- /dev/null +++ b/Swiftfin tvOS/Components/CinematicItemSelector.swift @@ -0,0 +1,245 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import Nuke +import SwiftUI + +struct CinematicItemSelector: View { + + @ObservedObject + private var viewModel: CinematicBackgroundView.ViewModel = .init() + + private var topContent: (Item) -> TopContent + private var itemContent: (Item) -> ItemContent + private var itemImageOverlay: (Item) -> ItemImageOverlay + private var itemContextMenu: (Item) -> ItemContextMenu + private var onSelect: (Item) -> Void + + let items: [Item] + + var body: some View { + ZStack(alignment: .bottomLeading) { + + ZStack { + CinematicBackgroundView(viewModel: viewModel, initialItem: items.first) + .ignoresSafeArea() + + LinearGradient( + stops: [ + .init(color: .clear, location: 0.5), + .init(color: .black.opacity(0.4), location: 0.6), + .init(color: .black, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + } + .mask { + LinearGradient( + stops: [ + .init(color: .white, location: 0.9), + .init(color: .clear, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + } + + VStack(alignment: .leading, spacing: 10) { + if let currentItem = viewModel.currentItem { + topContent(currentItem) + .id(currentItem.displayName) + } + + PosterHStack(type: .landscape, items: items) + .content(itemContent) + .imageOverlay(itemImageOverlay) + .contextMenu(itemContextMenu) + .onSelect(onSelect) + .onFocus { item in + viewModel.select(item: item) + } + } + } + .frame(height: UIScreen.main.bounds.height - 75) + .frame(maxWidth: .infinity) + } + + struct CinematicBackgroundView: UIViewRepresentable { + + @ObservedObject + var viewModel: ViewModel + var initialItem: Item? + + @ViewBuilder + private func imageView(for item: Item?) -> some View { + ImageView(item?.landscapePosterImageSources(maxWidth: UIScreen.main.bounds.width, single: false) ?? []) + } + + func makeUIView(context: Context) -> UIRotateImageView { + let hostingController = UIHostingController(rootView: imageView(for: initialItem), ignoreSafeArea: true) + return UIRotateImageView(initialView: hostingController.view) + } + + func updateUIView(_ uiView: UIRotateImageView, context: Context) { + let hostingController = UIHostingController(rootView: imageView(for: viewModel.currentItem), ignoreSafeArea: true) + uiView.update(with: hostingController.view) + } + + class ViewModel: ObservableObject { + + @Published + var currentItem: Item? + private var cancellables = Set() + + private var currentItemSubject = CurrentValueSubject(nil) + + init() { + currentItemSubject + .debounce(for: 0.5, scheduler: DispatchQueue.main) + .sink { newItem in + self.currentItem = newItem + } + .store(in: &cancellables) + } + + func select(item: Item) { + guard currentItem != item else { return } + currentItemSubject.send(item) + } + } + } + + class UIRotateImageView: UIView { + + private var currentView: UIView? + + init(initialView: UIView) { + super.init(frame: .zero) + + initialView.translatesAutoresizingMaskIntoConstraints = false + initialView.alpha = 0 + + addSubview(initialView) + NSLayoutConstraint.activate([ + initialView.topAnchor.constraint(equalTo: topAnchor), + initialView.bottomAnchor.constraint(equalTo: bottomAnchor), + initialView.leftAnchor.constraint(equalTo: leftAnchor), + initialView.rightAnchor.constraint(equalTo: rightAnchor), + ]) + + self.currentView = initialView + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(with newView: UIView) { + newView.translatesAutoresizingMaskIntoConstraints = false + newView.alpha = 0 + + addSubview(newView) + NSLayoutConstraint.activate([ + newView.topAnchor.constraint(equalTo: topAnchor), + newView.bottomAnchor.constraint(equalTo: bottomAnchor), + newView.leftAnchor.constraint(equalTo: leftAnchor), + newView.rightAnchor.constraint(equalTo: rightAnchor), + ]) + + UIView.animate(withDuration: 0.3) { + newView.alpha = 1 + self.currentView?.alpha = 0 + } completion: { _ in + self.currentView?.removeFromSuperview() + self.currentView = newView + } + } + } +} + +extension CinematicItemSelector where TopContent == EmptyView, + ItemContent == EmptyView, + ItemImageOverlay == EmptyView, + ItemContextMenu == EmptyView +{ + init(items: [Item]) { + self.init( + topContent: { _ in EmptyView() }, + itemContent: { _ in EmptyView() }, + itemImageOverlay: { _ in EmptyView() }, + itemContextMenu: { _ in EmptyView() }, + onSelect: { _ in }, + items: items + ) + } +} + +extension CinematicItemSelector { + + @ViewBuilder + func topContent(@ViewBuilder _ content: @escaping (Item) -> T) + -> CinematicItemSelector { + CinematicItemSelector( + topContent: content, + itemContent: itemContent, + itemImageOverlay: itemImageOverlay, + itemContextMenu: itemContextMenu, + onSelect: onSelect, + items: items + ) + } + + @ViewBuilder + func content(@ViewBuilder _ content: @escaping (Item) -> C) + -> CinematicItemSelector { + CinematicItemSelector( + topContent: topContent, + itemContent: content, + itemImageOverlay: itemImageOverlay, + itemContextMenu: itemContextMenu, + onSelect: onSelect, + items: items + ) + } + + @ViewBuilder + func itemImageOverlay(@ViewBuilder _ imageOverlay: @escaping (Item) -> O) + -> CinematicItemSelector { + CinematicItemSelector( + topContent: topContent, + itemContent: itemContent, + itemImageOverlay: imageOverlay, + itemContextMenu: itemContextMenu, + onSelect: onSelect, + items: items + ) + } + + @ViewBuilder + func contextMenu(@ViewBuilder _ contextMenu: @escaping (Item) -> M) + -> CinematicItemSelector { + CinematicItemSelector( + topContent: topContent, + itemContent: itemContent, + itemImageOverlay: itemImageOverlay, + itemContextMenu: contextMenu, + onSelect: onSelect, + items: items + ) + } + + func onSelect(_ action: @escaping (Item) -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } +} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift deleted file mode 100644 index b1718a12..00000000 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicBackgroundView.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import Nuke -import NukeExtensions -import SwiftUI -import UIKit - -class DynamicCinematicBackgroundViewModel: ObservableObject { - - @Published - var currentItem: BaseItemDto? - @Published - var currentImageView: UIImageView? - - @MainActor - func select(item: BaseItemDto) { - - guard item.id != currentItem?.id else { return } - - currentItem = item - - let itemImageView = UIImageView() - - let backdropImage: URL - - if item.type == .episode { - backdropImage = item.seriesImageURL(.backdrop, maxWidth: 1920) - } else { - backdropImage = item.imageURL(.backdrop, maxWidth: 1920) - } - - let options = ImageLoadingOptions(transition: .fadeIn(duration: 0.2)) - - 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 { - UICinematicBackgroundView(initialImageView: viewModel.currentImageView ?? UIImageView()) - } -} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift deleted file mode 100644 index 42033435..00000000 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicNextUpCardView.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct CinematicNextUpCardView: View { - - @EnvironmentObject - private var homeRouter: HomeCoordinator.Router - let item: BaseItemDto - let showOverlay: Bool - - var body: some View { - VStack(alignment: .leading) { - Button { - homeRouter.route(to: \.item, item) - } label: { - ZStack(alignment: .bottomLeading) { - - if item.type == .episode { - ImageView([ - item.seriesImageSource(.thumb, maxWidth: 350), - item.seriesImageSource(.backdrop, maxWidth: 350), - ]) - .frame(width: 350, height: 210) - } else { - ImageView([ - item.imageSource(.thumb, maxWidth: 350), - item.imageSource(.backdrop, 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) { - L10n.next.text - .font(.subheadline) - .padding(.vertical, 5) - .padding(.leading, 10) - .foregroundColor(.white) - - HStack { - Color.clear - .frame(width: 1, height: 7) - } - } - } - } - .frame(width: 350, height: 210) - } - .buttonStyle(.card) - .padding(.top) - } - .padding(.vertical) - } -} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift b/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift deleted file mode 100644 index 17b894b8..00000000 --- a/Swiftfin tvOS/Components/HomeCinematicView/CinematicResumeCardView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct CinematicResumeCardView: View { - - @EnvironmentObject - private var homeRouter: HomeCoordinator.Router - @ObservedObject - var viewModel: HomeViewModel - let item: BaseItemDto - - var body: some View { - VStack(alignment: .leading) { - Button { - homeRouter.route(to: \.item, item) - } label: { - ZStack(alignment: .bottom) { - - if item.type == .episode { - ImageView([ - item.seriesImageSource(.thumb, maxWidth: 350), - item.seriesImageSource(.backdrop, maxWidth: 350), - ]) - .frame(width: 350, height: 210) - } else { - ImageView([ - item.imageSource(.thumb, maxWidth: 350), - item.imageSource(.backdrop, 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.progress ?? "") - .font(.subheadline) - .padding(.vertical, 5) - .padding(.leading, 10) - .foregroundColor(.white) - - HStack { - Color.jellyfinPurple - .frame(width: 350 * (item.userData?.playedPercentage ?? 0) / 100, height: 7) - - Spacer(minLength: 0) - } - } - } - .frame(width: 350, height: 210) - } - .buttonStyle(.card) - .padding(.top) - .contextMenu { - Button(role: .destructive) { - viewModel.removeItemFromResume(item) - } label: { - L10n.removeFromResume.text - } - } - } - .padding(.vertical) - } -} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift b/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift deleted file mode 100644 index 54d234f8..00000000 --- a/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI -import UIKit - -// 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? - @ObservedObject - var viewModel: HomeViewModel - @State - private var updatedSelectedItem: BaseItemDto? - @State - private var initiallyAppeared = false - private let forcedItemSubtitle: String? - private let items: [HomeCinematicViewItem] - private let backgroundViewModel = DynamicCinematicBackgroundViewModel() - - init(viewModel: HomeViewModel, items: [HomeCinematicViewItem], forcedItemSubtitle: String? = nil) { - self.viewModel = viewModel - self.items = items - self.forcedItemSubtitle = forcedItemSubtitle - } - - var body: some View { - - ZStack(alignment: .bottom) { - - CinematicBackgroundView(viewModel: backgroundViewModel) - .frame(height: UIScreen.main.bounds.height - 50) - - 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?.type == .episode { - Text(updatedSelectedItem?.episodeLocator ?? "") - .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(viewModel: viewModel, 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 - } - } -} diff --git a/Swiftfin tvOS/Components/HomeCinematicView/UICinematicBackgroundView.swift b/Swiftfin tvOS/Components/HomeCinematicView/UICinematicBackgroundView.swift deleted file mode 100644 index 71d54879..00000000 --- a/Swiftfin tvOS/Components/HomeCinematicView/UICinematicBackgroundView.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & 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 - } - - @available(*, unavailable) - 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 - } - } -} diff --git a/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift b/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift new file mode 100644 index 00000000..7dffa7c8 --- /dev/null +++ b/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift @@ -0,0 +1,40 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct LandscapePosterProgressBar: View { + + let title: String + let progress: CGFloat + + var body: some View { + ZStack(alignment: .bottom) { + LinearGradient( + stops: [ + .init(color: .clear, location: 0.7), + .init(color: .black.opacity(0.7), location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + + VStack(alignment: .leading, spacing: 3) { + + Text(title) + .font(.subheadline) + .foregroundColor(.white) + + ProgressBar(progress: progress) + .frame(height: 5) + } + .padding(.horizontal, 5) + .padding(.bottom, 7) + } + } +} diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index 80144339..63798e79 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -10,42 +10,24 @@ import SwiftUI struct PosterButton: View { - private let item: Item - private let type: PosterType - private let itemScale: CGFloat - private let horizontalAlignment: HorizontalAlignment - private let content: (Item) -> Content - private let imageOverlay: (Item) -> ImageOverlay - private let contextMenu: (Item) -> ContextMenu - private let onSelect: (Item) -> Void - private let singleImage: Bool + @FocusState + private var isFocused: Bool + + private var item: Item + private var type: PosterType + private var itemScale: CGFloat + private var horizontalAlignment: HorizontalAlignment + private var content: (Item) -> Content + private var imageOverlay: (Item) -> ImageOverlay + private var contextMenu: (Item) -> ContextMenu + private var onSelect: (Item) -> Void + private var onFocus: () -> Void + private var singleImage: Bool private var itemWidth: CGFloat { type.width * itemScale } - private init( - item: Item, - type: PosterType, - itemScale: CGFloat, - horizontalAlignment: HorizontalAlignment, - @ViewBuilder content: @escaping (Item) -> Content, - @ViewBuilder imageOverlay: @escaping (Item) -> ImageOverlay, - @ViewBuilder contextMenu: @escaping (Item) -> ContextMenu, - onSelect: @escaping (Item) -> Void, - singleImage: Bool - ) { - self.item = item - self.type = type - self.itemScale = itemScale - self.horizontalAlignment = horizontalAlignment - self.content = content - self.imageOverlay = imageOverlay - self.contextMenu = contextMenu - self.onSelect = onSelect - self.singleImage = singleImage - } - var body: some View { VStack(alignment: horizontalAlignment) { Button { @@ -71,11 +53,16 @@ struct PosterButton, imageOverlay: { _ in EmptyView() }, contextMenu: { _ in EmptyView() }, onSelect: { _ in }, + onFocus: {}, singleImage: singleImage ) } } extension PosterButton { - @ViewBuilder - func horizontalAlignment(_ alignment: HorizontalAlignment) -> PosterButton { - PosterButton( - item: item, - type: type, - itemScale: itemScale, - horizontalAlignment: alignment, - content: content, - imageOverlay: imageOverlay, - contextMenu: contextMenu, - onSelect: onSelect, - singleImage: singleImage - ) + func horizontalAlignment(_ alignment: HorizontalAlignment) -> Self { + var copy = self + copy.horizontalAlignment = alignment + return copy } - @ViewBuilder - func scaleItem(_ scale: CGFloat) -> PosterButton { - PosterButton( - item: item, - type: type, - itemScale: scale, - horizontalAlignment: horizontalAlignment, - content: content, - imageOverlay: imageOverlay, - contextMenu: contextMenu, - onSelect: onSelect, - singleImage: singleImage - ) + func scaleItem(_ scale: CGFloat) -> Self { + var copy = self + copy.itemScale = scale + return copy } @ViewBuilder @@ -140,6 +110,7 @@ extension PosterButton { imageOverlay: imageOverlay, contextMenu: contextMenu, onSelect: onSelect, + onFocus: onFocus, singleImage: singleImage ) } @@ -155,6 +126,7 @@ extension PosterButton { imageOverlay: imageOverlay, contextMenu: contextMenu, onSelect: onSelect, + onFocus: onFocus, singleImage: singleImage ) } @@ -170,23 +142,21 @@ extension PosterButton { imageOverlay: imageOverlay, contextMenu: contextMenu, onSelect: onSelect, + onFocus: onFocus, singleImage: singleImage ) } - @ViewBuilder - func onSelect(_ action: @escaping (Item) -> Void) -> PosterButton { - PosterButton( - item: item, - type: type, - itemScale: itemScale, - horizontalAlignment: horizontalAlignment, - content: content, - imageOverlay: imageOverlay, - contextMenu: contextMenu, - onSelect: action, - singleImage: singleImage - ) + func onSelect(_ action: @escaping (Item) -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } + + func onFocus(_ action: @escaping () -> Void) -> Self { + var copy = self + copy.onFocus = action + return copy } } diff --git a/Swiftfin tvOS/Components/PosterHStack.swift b/Swiftfin tvOS/Components/PosterHStack.swift index a385dd87..aecffdb3 100644 --- a/Swiftfin tvOS/Components/PosterHStack.swift +++ b/Swiftfin tvOS/Components/PosterHStack.swift @@ -10,58 +10,42 @@ import SwiftUI struct PosterHStack: View { - private let title: String - private let type: PosterType - private let items: [Item] - private let itemScale: CGFloat - private let content: (Item) -> Content - private let imageOverlay: (Item) -> ImageOverlay - private let contextMenu: (Item) -> ContextMenu - private let trailingContent: () -> TrailingContent - private let onSelect: (Item) -> Void - - private init( - title: String, - type: PosterType, - items: [Item], - itemScale: CGFloat, - @ViewBuilder content: @escaping (Item) -> Content, - @ViewBuilder imageOverlay: @escaping (Item) -> ImageOverlay, - @ViewBuilder contextMenu: @escaping (Item) -> ContextMenu, - @ViewBuilder trailingContent: @escaping () -> TrailingContent, - onSelect: @escaping (Item) -> Void - ) { - self.title = title - self.type = type - self.items = items - self.itemScale = itemScale - self.content = content - self.imageOverlay = imageOverlay - self.contextMenu = contextMenu - self.trailingContent = trailingContent - self.onSelect = onSelect - } + private var title: String? + private var type: PosterType + private var items: [Item] + private var itemScale: CGFloat + private var content: (Item) -> Content + private var imageOverlay: (Item) -> ImageOverlay + private var contextMenu: (Item) -> ContextMenu + private var trailingContent: () -> TrailingContent + private var onSelect: (Item) -> Void + private var onFocus: (Item) -> Void var body: some View { VStack(alignment: .leading) { - HStack { - Text(title) - .font(.title2) - .fontWeight(.semibold) - .accessibility(addTraits: [.isHeader]) - .padding(.leading, 50) - Spacer() + if let title = title { + HStack { + Text(title) + .font(.title2) + .fontWeight(.semibold) + .accessibility(addTraits: [.isHeader]) + .padding(.leading, 50) + + Spacer() + } } ScrollView(.horizontal, showsIndicators: false) { - HStack(alignment: .top) { + HStack(alignment: .top, spacing: 30) { ForEach(items, id: \.hashValue) { item in PosterButton(item: item, type: type) .scaleItem(itemScale) + .content(content) .imageOverlay(imageOverlay) .contextMenu(contextMenu) .onSelect(onSelect) + .onFocus { onFocus(item) } } trailingContent() @@ -80,7 +64,7 @@ extension PosterHStack where Content == PosterButtonDefaultContentView, TrailingContent == EmptyView { init( - title: String, + title: String? = nil, type: PosterType, items: [Item] ) { @@ -93,25 +77,17 @@ extension PosterHStack where Content == PosterButtonDefaultContentView, imageOverlay: { _ in EmptyView() }, contextMenu: { _ in EmptyView() }, trailingContent: { EmptyView() }, - onSelect: { _ in } + onSelect: { _ in }, + onFocus: { _ in } ) } } extension PosterHStack { - @ViewBuilder - func scaleItems(_ scale: CGFloat) -> PosterHStack { - PosterHStack( - title: title, - type: type, - items: items, - itemScale: scale, - content: content, - imageOverlay: imageOverlay, - contextMenu: contextMenu, - trailingContent: trailingContent, - onSelect: onSelect - ) + func scaleItems(_ scale: CGFloat) -> Self { + var copy = self + copy.itemScale = scale + return copy } @ViewBuilder @@ -126,7 +102,8 @@ extension PosterHStack { imageOverlay: imageOverlay, contextMenu: contextMenu, trailingContent: trailingContent, - onSelect: onSelect + onSelect: onSelect, + onFocus: onFocus ) } @@ -142,7 +119,8 @@ extension PosterHStack { imageOverlay: imageOverlay, contextMenu: contextMenu, trailingContent: trailingContent, - onSelect: onSelect + onSelect: onSelect, + onFocus: onFocus ) } @@ -158,7 +136,8 @@ extension PosterHStack { imageOverlay: imageOverlay, contextMenu: contextMenu, trailingContent: trailingContent, - onSelect: onSelect + onSelect: onSelect, + onFocus: onFocus ) } @@ -174,22 +153,20 @@ extension PosterHStack { imageOverlay: imageOverlay, contextMenu: contextMenu, trailingContent: trailingContent, - onSelect: onSelect + onSelect: onSelect, + onFocus: onFocus ) } - @ViewBuilder - func onSelect(_ onSelect: @escaping (Item) -> Void) -> PosterHStack { - PosterHStack( - title: title, - type: type, - items: items, - itemScale: itemScale, - content: content, - imageOverlay: imageOverlay, - contextMenu: contextMenu, - trailingContent: trailingContent, - onSelect: onSelect - ) + func onSelect(_ action: @escaping (Item) -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } + + func onFocus(_ action: @escaping (Item) -> Void) -> Self { + var copy = self + copy.onFocus = action + return copy } } diff --git a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift deleted file mode 100644 index 4567d09a..00000000 --- a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct ContinueWatchingCard: View { - - @EnvironmentObject - private var homeRouter: HomeCoordinator.Router - let item: BaseItemDto - - var body: some View { - VStack(alignment: .leading) { - Button { - homeRouter.route(to: \.item, item) - } label: { - ZStack(alignment: .bottom) { - - if item.type == .episode { - ImageView([ - item.seriesImageSource(.thumb, maxWidth: 500), - item.imageSource(.primary, maxWidth: 500), - ]) - .frame(width: 500, height: 281.25) - } else { - ImageView(item.imageURL(.backdrop, maxWidth: 500)) - .frame(width: 500, height: 281.25) - } - - VStack(alignment: .leading, spacing: 0) { - Text(item.progress ?? "") - .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(.card) - .padding(.top) - - VStack(alignment: .leading) { - Text("\(item.seriesName ?? item.name ?? "")") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - .frame(width: 500, alignment: .leading) - - if item.type == .episode { - Text(item.episodeLocator ?? .emptyDash) - .font(.callout) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } else { - Text("") - } - } - } - .padding(.vertical) - } -} diff --git a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift deleted file mode 100644 index 389e7225..00000000 --- a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import JellyfinAPI -import Stinsen -import SwiftUI - -struct ContinueWatchingView: View { - - @EnvironmentObject - private 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(alignment: .top) { - ForEach(items, id: \.self) { item in - ContinueWatchingCard(item: item) - } - } - .padding(.horizontal, 50) - } - } - } -} diff --git a/Swiftfin tvOS/Views/HomeView.swift b/Swiftfin tvOS/Views/HomeView.swift deleted file mode 100644 index 2c1b0b59..00000000 --- a/Swiftfin tvOS/Views/HomeView.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Defaults -import Foundation -import Introspect -import JellyfinAPI -import SwiftUI - -struct HomeView: View { - - @EnvironmentObject - private var router: HomeCoordinator.Router - @ObservedObject - var viewModel: HomeViewModel - - var body: some View { - if viewModel.isLoading { - ProgressView() - .scaleEffect(2) - } else { - ScrollView { - LazyVStack(alignment: .leading) { - - if viewModel.resumeItems.isEmpty { - HomeCinematicView( - viewModel: viewModel, - items: viewModel.latestAddedItems.map { .init(item: $0, type: .plain) }, - forcedItemSubtitle: L10n.recentlyAdded - ) - - if !viewModel.nextUpItems.isEmpty { - PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems) - .onSelect { item in - router.route(to: \.item, item) - } - } - } else { - HomeCinematicView( - viewModel: viewModel, - items: viewModel.resumeItems.map { .init(item: $0, type: .resume) } - ) - - if !viewModel.nextUpItems.isEmpty { - PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems) - .onSelect { item in - router.route(to: \.item, item) - } - } - - if !viewModel.latestAddedItems.isEmpty { - PosterHStack(title: L10n.recentlyAdded, type: .portrait, items: viewModel.latestAddedItems) - .onSelect { item in - router.route(to: \.item, item) - } - } - } - - ForEach(viewModel.libraries, id: \.self) { library in - LatestInLibraryView(viewModel: LatestMediaViewModel(library: library)) - } - } - } - .edgesIgnoringSafeArea(.top) - .edgesIgnoringSafeArea(.horizontal) - } - } -} diff --git a/Swiftfin tvOS/Views/HomeView/HomeContentView.swift b/Swiftfin tvOS/Views/HomeView/HomeContentView.swift new file mode 100644 index 00000000..386ecf07 --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/HomeContentView.swift @@ -0,0 +1,131 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension HomeView { + + struct ContentView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel + + private func itemSelectorImageSource(for item: BaseItemDto) -> ImageSource { + if item.type == .episode { + return item.seriesImageSource( + .logo, + maxWidth: UIScreen.main.bounds.width * 0.4, + maxHeight: 200 + ) + } else { + return item.imageSource( + .logo, + maxWidth: UIScreen.main.bounds.width * 0.4, + maxHeight: 200 + ) + } + } + + @ViewBuilder + private var cinematicResumeItems: some View { + CinematicItemSelector(items: viewModel.resumeItems) + .topContent { item in + ImageView(itemSelectorImageSource(for: item)) + .resizingMode(.bottomLeft) + .placeholder { + EmptyView() + } + .failure { + Text(item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + } + .padding2(.leading) + } + .content { item in + if let subtitle = item.subtitle { + Text(subtitle) + .font(.caption) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(2) + } + } + .itemImageOverlay { item in + LandscapePosterProgressBar( + title: item.progress ?? L10n.continue, + progress: (item.userData?.playedPercentage ?? 0) / 100 + ) + } + .onSelect { item in + router.route(to: \.item, item) + } + } + + @ViewBuilder + private var cinematicLatestAddedItems: some View { + CinematicItemSelector(items: viewModel.latestAddedItems) + .topContent { item in + ImageView(itemSelectorImageSource(for: item)) + .resizingMode(.bottomLeft) + .placeholder { + EmptyView() + } + .failure { + Text(item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + } + .padding2(.leading) + } + .onSelect { item in + router.route(to: \.item, item) + } + } + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading) { + if viewModel.resumeItems.isEmpty { + cinematicLatestAddedItems + + if !viewModel.nextUpItems.isEmpty { + PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems) + .onSelect { item in + router.route(to: \.item, item) + } + } + } else { + cinematicResumeItems + + if !viewModel.nextUpItems.isEmpty { + PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems) + .onSelect { item in + router.route(to: \.item, item) + } + } + + if !viewModel.latestAddedItems.isEmpty { + PosterHStack(title: L10n.recentlyAdded, type: .portrait, items: viewModel.latestAddedItems) + .onSelect { item in + router.route(to: \.item, item) + } + } + } + + ForEach(viewModel.libraries, id: \.self) { library in + LatestInLibraryView(viewModel: LatestMediaViewModel(library: library)) + } + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift b/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift new file mode 100644 index 00000000..765e4193 --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift @@ -0,0 +1,53 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension HomeView { + + struct ErrorView: View { + + @ObservedObject + var viewModel: HomeViewModel + + let errorMessage: ErrorMessage + + var body: some View { + VStack { + if viewModel.isLoading { + ProgressView() + .frame(width: 100, height: 100) + .scaleEffect(2) + } else { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 72)) + .foregroundColor(Color.red) + .frame(width: 100, height: 100) + } + + Text("\(errorMessage.code)") + + Text(errorMessage.message) + .frame(minWidth: 50, maxWidth: 240) + .multilineTextAlignment(.center) + + Button { + viewModel.refresh() + } label: { + L10n.retry.text + .bold() + .font(.callout) + .frame(width: 400, height: 75) + .background(Color.jellyfinPurple) + } + .buttonStyle(.card) + } + .offset(y: -50) + } + } +} diff --git a/Swiftfin tvOS/Views/HomeView/HomeView.swift b/Swiftfin tvOS/Views/HomeView/HomeView.swift new file mode 100644 index 00000000..63a49ad1 --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/HomeView.swift @@ -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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import Introspect +import JellyfinAPI +import SwiftUI + +struct HomeView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel + + var body: some View { + Group { + if let errorMessage = viewModel.errorMessage { + ErrorView(viewModel: viewModel, errorMessage: errorMessage) + } else if viewModel.isLoading { + ProgressView() + } else { + ContentView(viewModel: viewModel) + } + } + .edgesIgnoringSafeArea(.top) + .edgesIgnoringSafeArea(.horizontal) + } +} diff --git a/Swiftfin tvOS/Views/LatestInLibraryView.swift b/Swiftfin tvOS/Views/LatestInLibraryView.swift index 188dfccd..1965adbb 100644 --- a/Swiftfin tvOS/Views/LatestInLibraryView.swift +++ b/Swiftfin tvOS/Views/LatestInLibraryView.swift @@ -34,7 +34,7 @@ struct LatestInLibraryView: View { .font(.title3) } } - .posterStyle(type: .portrait, width: 250) + .posterStyle(type: .portrait, width: PosterType.portrait.width) } .buttonStyle(.plain) } diff --git a/Swiftfin tvOS/Views/LibraryView.swift b/Swiftfin tvOS/Views/LibraryView.swift index e871f3ba..d93a6b9d 100644 --- a/Swiftfin tvOS/Views/LibraryView.swift +++ b/Swiftfin tvOS/Views/LibraryView.swift @@ -44,7 +44,7 @@ struct LibraryView: View { .layout { _, layoutEnvironment in .grid( layoutEnvironment: layoutEnvironment, - layoutMode: .fixedNumberOfColumns(6), + layoutMode: .fixedNumberOfColumns(7), lineSpacing: 50 ) } diff --git a/Swiftfin tvOS/Views/MediaView.swift b/Swiftfin tvOS/Views/MediaView.swift index a96cd12d..c7f31cc1 100644 --- a/Swiftfin tvOS/Views/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView.swift @@ -23,7 +23,7 @@ struct MediaView: View { var body: some View { CollectionView(items: viewModel.libraryItems) { _, item, _ in PosterButton(item: item, type: .landscape) - .scaleItem(0.8) + .scaleItem(1.12) .onSelect { _ in switch item.library.collectionType { case "favorites": diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 64af33a0..0b77bdbb 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; - 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */; }; 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; @@ -221,11 +220,6 @@ E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; }; E1002B682793CFBA00E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B672793CFBA00E47059 /* Algorithms */; }; E1002B6B2793E36600E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B6A2793E36600E47059 /* Algorithms */; }; - 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 */; }; E1047E2327E5880000CB0D4A /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; }; E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; @@ -436,6 +430,10 @@ E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C157279A7D76005EC829 /* BundleExtensions.swift */; }; E1A2C15A279A7D76005EC829 /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C157279A7D76005EC829 /* BundleExtensions.swift */; }; E1A2C160279A7DCA005EC829 /* AboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */; }; + E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */; }; + E1A42E4C28CBD39300A14DCB /* HomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4B28CBD39300A14DCB /* HomeContentView.swift */; }; + E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */; }; + E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */; }; E1AA331D2782541500F6439C /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331C2782541500F6439C /* PrimaryButton.swift */; }; E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; E1AA33202782639D00F6439C /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; @@ -443,7 +441,6 @@ E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; E1B2AB9928808E150072B3B9 /* GoogleCast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1B2AB9628808CDF0072B3B9 /* GoogleCast.xcframework */; }; - E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */; }; E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE9271A23880015B715 /* SwiftyJSON */; }; E1BDE359278E9ED2004E4022 /* MissingItemsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDE358278E9ED2004E4022 /* MissingItemsSettingsView.swift */; }; E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; @@ -531,7 +528,6 @@ E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; }; E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A628C29B720021BC93 /* ProgressBar.swift */; }; E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */; }; - E1FE69AB28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -584,7 +580,6 @@ 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = ""; }; 09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = ""; }; 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeItemElement.swift; sourceTree = ""; }; 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = ""; }; 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; @@ -750,11 +745,6 @@ C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = ""; }; E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfoExtensions.swift; sourceTree = ""; }; - E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICinematicBackgroundView.swift; sourceTree = ""; }; - E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicBackgroundView.swift; sourceTree = ""; }; - E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCinematicView.swift; sourceTree = ""; }; - E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeCardView.swift; sourceTree = ""; }; - E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicNextUpCardView.swift; sourceTree = ""; }; E1047E2227E5880000CB0D4A /* InitialFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialFailureView.swift; sourceTree = ""; }; E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = ""; }; E10D87DB2784EC5200BD264C /* SeriesEpisodesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesEpisodesView.swift; sourceTree = ""; }; @@ -890,12 +880,15 @@ E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtensions.swift; sourceTree = ""; }; E1A2C157279A7D76005EC829 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = ""; }; E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = ""; }; + E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = ""; }; + E1A42E4B28CBD39300A14DCB /* HomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = ""; }; + E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = ""; }; + E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = ""; }; E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = ""; }; E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = ""; }; E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = ""; }; E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGUIDPairExtensions.swift; sourceTree = ""; }; E1B2AB9628808CDF0072B3B9 /* GoogleCast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GoogleCast.xcframework; path = Carthage/Build/GoogleCast.xcframework; sourceTree = ""; }; - E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = ""; }; E1BDE358278E9ED2004E4022 /* MissingItemsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissingItemsSettingsView.swift; sourceTree = ""; }; E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = ""; }; E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = ""; }; @@ -1219,16 +1212,17 @@ 536D3D77267BB9650004248C /* Components */ = { isa = PBXGroup; children = ( + E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */, E1C92618288756BD002A7A66 /* DotHStack.swift */, - E103A6A1278A7EB500820EC7 /* HomeCinematicView */, E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */, - E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */, 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, + E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */, 536D3D80267BDFC60004248C /* PortraitItemElement.swift */, E1C92617288756BD002A7A66 /* PosterButton.swift */, E1C92619288756BD002A7A66 /* PosterHStack.swift */, E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, + E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */, ); path = Components; sourceTree = ""; @@ -1460,6 +1454,7 @@ children = ( E18E01A7288746AF0022598C /* DotHStack.swift */, E1FE69AF28C2DA4A0021BC93 /* FilterDrawerHStack */, + E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */, E18E01A5288746AF0022598C /* PillHStack.swift */, E16AA60728A364A6009A983C /* PosterButton.swift */, E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */, @@ -1577,18 +1572,6 @@ path = Overlays; sourceTree = ""; }; - E103A6A1278A7EB500820EC7 /* HomeCinematicView */ = { - isa = PBXGroup; - children = ( - E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */, - E103A6A8278AB6FF00820EC7 /* CinematicNextUpCardView.swift */, - E103A6A6278AB6D700820EC7 /* CinematicResumeCardView.swift */, - E103A6A4278A82E500820EC7 /* HomeCinematicView.swift */, - E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */, - ); - path = HomeCinematicView; - sourceTree = ""; - }; E107BB9127880A4000354E07 /* ItemViewModel */ = { isa = PBXGroup; children = ( @@ -1681,8 +1664,7 @@ E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */, E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */, 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, - E1B59FD62786AE2C00A5287E /* ContinueWatchingView */, - 531690E6267ABD79005D8AB9 /* HomeView.swift */, + E1A42E4D28CBD3B200A14DCB /* HomeView */, E193D54E271942C000900D82 /* ItemView */, E1C925F828875647002A7A66 /* LatestInLibraryView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, @@ -2038,6 +2020,16 @@ path = AboutView; sourceTree = ""; }; + E1A42E4D28CBD3B200A14DCB /* HomeView */ = { + isa = PBXGroup; + children = ( + E1A42E4B28CBD39300A14DCB /* HomeContentView.swift */, + E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */, + 531690E6267ABD79005D8AB9 /* HomeView.swift */, + ); + path = HomeView; + sourceTree = ""; + }; E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { isa = PBXGroup; children = ( @@ -2069,7 +2061,6 @@ E18E01FF288749200022598C /* Divider.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, E1047E2227E5880000CB0D4A /* InitialFailureView.swift */, - E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */, 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, E1FE69A628C29B720021BC93 /* ProgressBar.swift */, E1E1643D28BB074000323B0A /* SelectorView.swift */, @@ -2078,15 +2069,6 @@ path = Views; sourceTree = ""; }; - E1B59FD62786AE2C00A5287E /* ContinueWatchingView */ = { - isa = PBXGroup; - children = ( - E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */, - 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */, - ); - path = ContinueWatchingView; - sourceTree = ""; - }; E1C55AB228BD051700A9AD88 /* Components */ = { isa = PBXGroup; children = ( @@ -2466,7 +2448,6 @@ C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */, E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, - E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */, C4B9B91427E1921B0063535C /* LiveTVNativeVideoPlayerView.swift in Sources */, E18E021E2887492B0022598C /* Divider.swift in Sources */, E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, @@ -2498,20 +2479,17 @@ 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */, E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */, + E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, - E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, E12B835F28C07D8500878399 /* LibraryParent.swift in Sources */, - E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */, E18E021A2887492B0022598C /* AppIcon.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */, - E1FE69AB28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, - 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, @@ -2542,9 +2520,9 @@ C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */, E18E023C288749540022598C /* UIScrollViewExtensions.swift in Sources */, + E1A42E4C28CBD39300A14DCB /* HomeContentView.swift in Sources */, C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */, E1C9260F2887565C002A7A66 /* AttributeHStack.swift in Sources */, - E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */, E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */, E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */, E148128328C1443D003B8787 /* NameGUIDPairExtensions.swift in Sources */, @@ -2562,6 +2540,7 @@ E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, E18E02202887492B0022598C /* AttributeFillView.swift in Sources */, E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, + E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */, E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, E148128C28C15526003B8787 /* SortBy.swift in Sources */, E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */, @@ -2574,7 +2553,6 @@ 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */, E13F05F228BC9016003499D2 /* LibraryItemRow.swift in Sources */, - E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */, 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, @@ -2605,7 +2583,6 @@ E18E02232887492B0022598C /* ImageView.swift in Sources */, E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, - E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */, E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */, E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */, E1E1643A28BAC2EF00323B0A /* SearchView.swift in Sources */, @@ -2646,6 +2623,7 @@ 53ABFDE4267974EF00886593 /* MediaViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, E1937A62288F32DB00CB80AA /* Poster.swift in Sources */, + E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */, C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */, E1A2C160279A7DCA005EC829 /* AboutAppView.swift in Sources */, diff --git a/Shared/Views/LandscapePosterProgressBar.swift b/Swiftfin/Components/LandscapePosterProgressBar.swift similarity index 97% rename from Shared/Views/LandscapePosterProgressBar.swift rename to Swiftfin/Components/LandscapePosterProgressBar.swift index df73f418..e541b307 100644 --- a/Shared/Views/LandscapePosterProgressBar.swift +++ b/Swiftfin/Components/LandscapePosterProgressBar.swift @@ -39,6 +39,7 @@ struct LandscapePosterProgressBar: View { .foregroundColor(.white) ProgressBar(progress: progress) + .frame(height: 3) } .padding(.horizontal, 5 * paddingScale) .padding(.bottom, 7 * paddingScale)