135 lines
4.6 KiB
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
|
|
}
|
|
}
|
|
}
|