mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 14:10:01 +00:00
tvOS - Refactor Cinematic Item Selector (#564)
This commit is contained in:
parent
859a47803f
commit
3c81c7532f
@ -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))
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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
|
||||
|
@ -23,6 +23,5 @@ struct ProgressBar: View {
|
||||
.scaleEffect(x: progress, y: 1, anchor: .leading)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 3)
|
||||
}
|
||||
}
|
||||
|
245
Swiftfin tvOS/Components/CinematicItemSelector.swift
Normal file
245
Swiftfin tvOS/Components/CinematicItemSelector.swift
Normal file
@ -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<Item: Poster, TopContent: View, ItemContent: View, ItemImageOverlay: View, ItemContextMenu: View>: 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<AnyCancellable>()
|
||||
|
||||
private var currentItemSubject = CurrentValueSubject<Item?, Never>(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<T: View>(@ViewBuilder _ content: @escaping (Item) -> T)
|
||||
-> CinematicItemSelector<Item, T, ItemContent, ItemImageOverlay, ItemContextMenu> {
|
||||
CinematicItemSelector<Item, T, ItemContent, ItemImageOverlay, ItemContextMenu>(
|
||||
topContent: content,
|
||||
itemContent: itemContent,
|
||||
itemImageOverlay: itemImageOverlay,
|
||||
itemContextMenu: itemContextMenu,
|
||||
onSelect: onSelect,
|
||||
items: items
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func content<C: View>(@ViewBuilder _ content: @escaping (Item) -> C)
|
||||
-> CinematicItemSelector<Item, TopContent, C, ItemImageOverlay, ItemContextMenu> {
|
||||
CinematicItemSelector<Item, TopContent, C, ItemImageOverlay, ItemContextMenu>(
|
||||
topContent: topContent,
|
||||
itemContent: content,
|
||||
itemImageOverlay: itemImageOverlay,
|
||||
itemContextMenu: itemContextMenu,
|
||||
onSelect: onSelect,
|
||||
items: items
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func itemImageOverlay<O: View>(@ViewBuilder _ imageOverlay: @escaping (Item) -> O)
|
||||
-> CinematicItemSelector<Item, TopContent, ItemContent, O, ItemContextMenu> {
|
||||
CinematicItemSelector<Item, TopContent, ItemContent, O, ItemContextMenu>(
|
||||
topContent: topContent,
|
||||
itemContent: itemContent,
|
||||
itemImageOverlay: imageOverlay,
|
||||
itemContextMenu: itemContextMenu,
|
||||
onSelect: onSelect,
|
||||
items: items
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func contextMenu<M: View>(@ViewBuilder _ contextMenu: @escaping (Item) -> M)
|
||||
-> CinematicItemSelector<Item, TopContent, ItemContent, ItemImageOverlay, M> {
|
||||
CinematicItemSelector<Item, TopContent, ItemContent, ItemImageOverlay, M>(
|
||||
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
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
40
Swiftfin tvOS/Components/LandscapePosterProgressBar.swift
Normal file
40
Swiftfin tvOS/Components/LandscapePosterProgressBar.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -10,42 +10,24 @@ import SwiftUI
|
||||
|
||||
struct PosterButton<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View>: 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<Item: Poster, Content: View, ImageOverlay: View, ContextMenu
|
||||
contextMenu(item)
|
||||
})
|
||||
.posterShadow()
|
||||
.focused($isFocused)
|
||||
|
||||
content(item)
|
||||
.zIndex(-1)
|
||||
}
|
||||
.frame(width: itemWidth)
|
||||
.onChange(of: isFocused) { newValue in
|
||||
guard newValue else { return }
|
||||
onFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,40 +80,23 @@ extension PosterButton where Content == PosterButtonDefaultContentView<Item>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,58 +10,42 @@ import SwiftUI
|
||||
|
||||
struct PosterHStack<Item: Poster, Content: View, ImageOverlay: View, ContextMenu: View, TrailingContent: View>: 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<Item>,
|
||||
TrailingContent == EmptyView
|
||||
{
|
||||
init(
|
||||
title: String,
|
||||
title: String? = nil,
|
||||
type: PosterType,
|
||||
items: [Item]
|
||||
) {
|
||||
@ -93,25 +77,17 @@ extension PosterHStack where Content == PosterButtonDefaultContentView<Item>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
131
Swiftfin tvOS/Views/HomeView/HomeContentView.swift
Normal file
131
Swiftfin tvOS/Views/HomeView/HomeContentView.swift
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
Swiftfin tvOS/Views/HomeView/HomeErrorView.swift
Normal file
53
Swiftfin tvOS/Views/HomeView/HomeErrorView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
35
Swiftfin tvOS/Views/HomeView/HomeView.swift
Normal file
35
Swiftfin tvOS/Views/HomeView/HomeView.swift
Normal 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 (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)
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ struct LatestInLibraryView: View {
|
||||
.font(.title3)
|
||||
}
|
||||
}
|
||||
.posterStyle(type: .portrait, width: 250)
|
||||
.posterStyle(type: .portrait, width: PosterType.portrait.width)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ struct LibraryView: View {
|
||||
.layout { _, layoutEnvironment in
|
||||
.grid(
|
||||
layoutEnvironment: layoutEnvironment,
|
||||
layoutMode: .fixedNumberOfColumns(6),
|
||||
layoutMode: .fixedNumberOfColumns(7),
|
||||
lineSpacing: 50
|
||||
)
|
||||
}
|
||||
|
@ -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":
|
||||
|
@ -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 = "<group>"; };
|
||||
09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = "<group>"; };
|
||||
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
||||
531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeItemElement.swift; sourceTree = "<group>"; };
|
||||
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = "<group>"; };
|
||||
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = "<group>"; };
|
||||
@ -750,11 +745,6 @@
|
||||
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = "<group>"; };
|
||||
E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfoExtensions.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>"; };
|
||||
E1047E2227E5880000CB0D4A /* InitialFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialFailureView.swift; sourceTree = "<group>"; };
|
||||
E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; };
|
||||
E10D87DB2784EC5200BD264C /* SeriesEpisodesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesEpisodesView.swift; sourceTree = "<group>"; };
|
||||
@ -890,12 +880,15 @@
|
||||
E1A2C153279A7D5A005EC829 /* UIApplicationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtensions.swift; sourceTree = "<group>"; };
|
||||
E1A2C157279A7D76005EC829 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = "<group>"; };
|
||||
E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutAppView.swift; sourceTree = "<group>"; };
|
||||
E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = "<group>"; };
|
||||
E1A42E4B28CBD39300A14DCB /* HomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = "<group>"; };
|
||||
E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = "<group>"; };
|
||||
E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = "<group>"; };
|
||||
E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
|
||||
E1AA331E2782639D00F6439C /* OverlayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayType.swift; sourceTree = "<group>"; };
|
||||
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = "<group>"; };
|
||||
E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGUIDPairExtensions.swift; sourceTree = "<group>"; };
|
||||
E1B2AB9628808CDF0072B3B9 /* GoogleCast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GoogleCast.xcframework; path = Carthage/Build/GoogleCast.xcframework; sourceTree = "<group>"; };
|
||||
E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = "<group>"; };
|
||||
E1BDE358278E9ED2004E4022 /* MissingItemsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissingItemsSettingsView.swift; sourceTree = "<group>"; };
|
||||
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = "<group>"; };
|
||||
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerOverlayDelegate.swift; sourceTree = "<group>"; };
|
||||
@ -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 = "<group>";
|
||||
@ -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 = "<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 = (
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
E1A42E4D28CBD3B200A14DCB /* HomeView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1A42E4B28CBD39300A14DCB /* HomeContentView.swift */,
|
||||
E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */,
|
||||
531690E6267ABD79005D8AB9 /* HomeView.swift */,
|
||||
);
|
||||
path = HomeView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
E1B59FD62786AE2C00A5287E /* ContinueWatchingView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */,
|
||||
531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */,
|
||||
);
|
||||
path = ContinueWatchingView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
@ -39,6 +39,7 @@ struct LandscapePosterProgressBar: View {
|
||||
.foregroundColor(.white)
|
||||
|
||||
ProgressBar(progress: progress)
|
||||
.frame(height: 3)
|
||||
}
|
||||
.padding(.horizontal, 5 * paddingScale)
|
||||
.padding(.bottom, 7 * paddingScale)
|
Loading…
Reference in New Issue
Block a user