jellyflood/Swiftfin tvOS/Components/HomeCinematicView/HomeCinematicView.swift

135 lines
4.6 KiB
Swift

//
// 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
}
}
}