jellyflood/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift
Joe Kribs 216375905c
[iOS & tvOS] Trailers (#1456)
* ItemViewModel Trailers

* iOS done.

* Sections >>> Divider

* tvOS kind of.

* Button/Menu cleanup

* Huge ActionButton overhaul

* Error Handling, ActionButton/Menu standardization, and ActionButtonLayout cleanup part 1.

* cleanup

* cleanup

* Combine ActionButton logic. Complete ActionButton rework and animation/style rework. Should this be 3 files??

* Dumb sizing error. Get size from WIDTH not HEIGHT! Height is always 100 and Width is larger.

* Pressed buttons are but focused buttons but slight less. Pressed buttons are still bigger than default, unfocused buttons. TIL.

* Cleanup / Structure

* Remove Test.

* New Setting. Version on PlayButton Row. Complete TrailerMenu revamp. Make ActionButtonLayout a single row.

* Spacing & remove test logic

* VERY WIP

* Fix the compact-ness

* Linting.

* Remove Testing logic.

* Pre-Cleanup - WIP

* Finalized. Moved ScrollingText to tvOS Only.

* MediaURL? = nil but it's already nil by default.

* Error on the View not the button. This was NOT showing for the button since it lived on the Menu. This resolves this.

* wip

* Update VersionMenu.swift

* Remove scrollingText from this PR.

* Remove labels & iOS Action Button cleanup / no foregroundStyle on de-selected.

* ActionButtonScaling

* .card all buttons in ActionButton

* Slow and less bounce-i-fy the menu animations. Also, slight padding

* Wait, don't add this padding this isn't needed.

* localize

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
2025-04-05 01:13:20 -04:00

141 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) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
extension ItemView {
struct CinematicScrollView<Content: View>: View {
@ObservedObject
var viewModel: ItemViewModel
let content: () -> Content
var body: some View {
ZStack {
if viewModel.item.type == .episode {
ImageView(viewModel.item.imageSource(.primary, maxWidth: 1920))
} else {
ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920))
}
ScrollView(.vertical, showsIndicators: false) {
content()
}
}
.ignoresSafeArea()
}
}
}
extension ItemView {
struct CinematicHeaderView: View {
enum CinematicHeaderFocusLayer: Hashable {
case top
case playButton
}
@EnvironmentObject
private var router: ItemCoordinator.Router
@ObservedObject
var viewModel: ItemViewModel
@FocusState
private var focusedLayer: CinematicHeaderFocusLayer?
var body: some View {
VStack(alignment: .leading) {
Color.clear
.focusable()
.focused($focusedLayer, equals: .top)
HStack(alignment: .bottom) {
VStack(alignment: .leading, spacing: 20) {
ImageView(viewModel.item.imageSource(
.logo,
maxWidth: UIScreen.main.bounds.width * 0.4,
maxHeight: 250
))
.placeholder { _ in
EmptyView()
}
.failure {
Text(viewModel.item.displayTitle)
.font(.largeTitle)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.leading)
.foregroundColor(.white)
}
.aspectRatio(contentMode: .fit)
.padding(.bottom)
.frame(maxHeight: 250, alignment: .bottomLeading)
if let tagline = viewModel.item.taglines?.first {
Text(tagline)
.font(.subheadline)
.fontWeight(.semibold)
.multilineTextAlignment(.leading)
.lineLimit(1)
}
Text(viewModel.item.overview ?? L10n.noOverviewAvailable)
.font(.subheadline)
.lineLimit(3)
HStack {
DotHStack {
if let firstGenre = viewModel.item.genres?.first {
Text(firstGenre)
}
if let premiereYear = viewModel.item.premiereDateYear {
Text(premiereYear)
}
if let playButtonitem = viewModel.playButtonItem, let runtime = playButtonitem.runTimeLabel {
Text(runtime)
}
}
.font(.caption)
.foregroundColor(Color(UIColor.lightGray))
ItemView.AttributesHStack(viewModel: viewModel)
}
}
Spacer()
VStack {
ItemView.PlayButton(viewModel: viewModel)
.focused($focusedLayer, equals: .playButton)
ItemView.ActionButtonHStack(viewModel: viewModel)
.frame(width: 440)
}
.frame(width: 450)
.padding(.leading, 150)
}
}
.padding(.horizontal, 50)
.onChange(of: focusedLayer) { _, layer in
if layer == .top {
focusedLayer = .playButton
}
}
}
}
}