jellyflood/Swiftfin/Views/ItemView/Components/ActionButtonHStack/Components/TrailerMenu.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.2 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 Factory
import JellyfinAPI
import SwiftUI
extension ItemView {
struct TrailerMenu: View {
@Injected(\.logService)
private var logger
// MARK: - Stored Value
@StoredValue(.User.enabledTrailers)
private var enabledTrailers: TrailerSelection
// MARK: - Observed & Envirnoment Objects
@EnvironmentObject
private var router: MainCoordinator.Router
// MARK: - Error State
@State
private var error: Error?
let localTrailers: [BaseItemDto]
let externalTrailers: [MediaURL]
private var showLocalTrailers: Bool {
enabledTrailers.contains(.local) && localTrailers.isNotEmpty
}
private var showExternalTrailers: Bool {
enabledTrailers.contains(.external) && externalTrailers.isNotEmpty
}
// MARK: - Body
var body: some View {
Group {
switch localTrailers.count + externalTrailers.count {
case 1:
trailerButton
default:
trailerMenu
}
}
.errorMessage($error)
}
// MARK: - Single Trailer Button
@ViewBuilder
private var trailerButton: some View {
ActionButton(
L10n.trailers,
icon: "movieclapper"
) {
if showLocalTrailers, let firstTrailer = localTrailers.first {
playLocalTrailer(firstTrailer)
}
if showExternalTrailers, let firstTrailer = externalTrailers.first {
playExternalTrailer(firstTrailer)
}
}
}
// MARK: - Multiple Trailers Menu Button
@ViewBuilder
private var trailerMenu: some View {
ActionButton(L10n.trailers, icon: "movieclapper") {
if showLocalTrailers {
Section(L10n.local) {
ForEach(localTrailers) { trailer in
Button(
trailer.name ?? L10n.trailer,
systemImage: "play.fill"
) {
playLocalTrailer(trailer)
}
}
}
}
if showExternalTrailers {
Section(L10n.external) {
ForEach(externalTrailers, id: \.self) { mediaURL in
Button(
mediaURL.name ?? L10n.trailer,
systemImage: "arrow.up.forward"
) {
playExternalTrailer(mediaURL)
}
}
}
}
}
}
// MARK: - Play: Local Trailer
private func playLocalTrailer(_ trailer: BaseItemDto) {
if let selectedMediaSource = trailer.mediaSources?.first {
router.route(
to: \.videoPlayer,
OnlineVideoPlayerManager(item: trailer, mediaSource: selectedMediaSource)
)
} else {
logger.log(level: .error, "No media sources found")
error = JellyfinAPIError(L10n.unknownError)
}
}
// MARK: - Play: External Trailer
private func playExternalTrailer(_ trailer: MediaURL) {
if let url = URL(string: trailer.url), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url) { success in
guard !success else { return }
error = JellyfinAPIError(L10n.unableToOpenTrailer)
}
} else {
error = JellyfinAPIError(L10n.unableToOpenTrailer)
}
}
}
}