jellyflood/Swiftfin/Views/ItemView/Components/PlayButton.swift

135 lines
3.9 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 JellyfinAPI
import Logging
import SwiftUI
extension ItemView {
struct PlayButton: View {
@Default(.accentColor)
private var accentColor
@Router
private var router
@ObservedObject
var viewModel: ItemViewModel
private let logger = Logger.swiftfin()
// MARK: - Validation
private var isEnabled: Bool {
viewModel.selectedMediaSource != nil
}
// MARK: - Title
private var title: String {
/// Use the Season/Episode label for the Series ItemView
if let seriesViewModel = viewModel as? SeriesItemViewModel,
let seasonEpisodeLabel = seriesViewModel.playButtonItem?.seasonEpisodeLabel
{
return seasonEpisodeLabel
/// Use a Play/Resume label for single Media Source items that are not Series
} else if let playButtonLabel = viewModel.playButtonItem?.playButtonLabel {
return playButtonLabel
/// Fallback to a generic `Play` label
} else {
return L10n.play
}
}
// MARK: - Media Source
private var source: String? {
guard let sourceLabel = viewModel.selectedMediaSource?.displayTitle,
viewModel.item.mediaSources?.count ?? 0 > 1
else {
return nil
}
return sourceLabel
}
// MARK: - Body
var body: some View {
Button {
play()
} label: {
HStack {
Label(title, systemImage: "play.fill")
.font(.callout)
.fontWeight(.semibold)
if let source {
Marquee(source, speed: 40, delay: 3, fade: 5)
.font(.caption)
.fontWeight(.medium)
.frame(maxWidth: 175)
}
}
.padding(.horizontal, 5)
}
.buttonStyle(.tintedMaterial(tint: accentColor, foregroundColor: accentColor.overlayColor))
.isSelected(true)
.contextMenu {
if viewModel.playButtonItem?.userData?.playbackPositionTicks != 0 {
Button(L10n.playFromBeginning, systemImage: "gobackward") {
play(fromBeginning: true)
}
}
}
.disabled(!isEnabled)
}
// MARK: - Play Content
private func play(fromBeginning: Bool = false) {
guard let playButtonItem = viewModel.playButtonItem,
let selectedMediaSource = viewModel.selectedMediaSource
else {
logger.error("Play selected with no item or media source")
return
}
let queue: (any MediaPlayerQueue)? = {
if playButtonItem.type == .episode {
return EpisodeMediaPlayerQueue(episode: playButtonItem)
}
return nil
}()
let provider = MediaPlayerItemProvider(item: playButtonItem) { item in
try await MediaPlayerItem.build(
for: item,
mediaSource: selectedMediaSource
) {
if fromBeginning {
$0.userData?.playbackPositionTicks = 0
}
}
}
router.route(
to: .videoPlayer(
provider: provider,
queue: queue
)
)
}
}
}