jellyflood/Shared/Objects/MediaPlayerManager/Supplements/MediaInfoSupplement.swift

178 lines
5.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 JellyfinAPI
import SwiftUI
// TODO: scroll if description too long
struct MediaInfoSupplement: MediaPlayerSupplement {
let displayTitle: String = "Info"
let item: BaseItemDto
var id: String {
"MediaInfo-\(item.id ?? "any")"
}
var videoPlayerBody: some PlatformView {
InfoOverlay(item: item)
}
}
extension MediaInfoSupplement {
private struct InfoOverlay: PlatformView {
@Environment(\.safeAreaInsets)
private var safeAreaInsets: EdgeInsets
@EnvironmentObject
private var containerState: VideoPlayerContainerState
@EnvironmentObject
private var manager: MediaPlayerManager
let item: BaseItemDto
@ViewBuilder
private var accessoryView: some View {
DotHStack {
if item.type == .episode, let seasonEpisodeLocator = item.seasonEpisodeLabel {
Text(seasonEpisodeLocator)
} else if let premiereYear = item.premiereDateYear {
Text(premiereYear)
}
if let runtime = item.runTimeLabel {
Text(runtime)
}
if let officialRating = item.officialRating {
Text(officialRating)
}
}
}
@ViewBuilder
private var fromBeginningButton: some View {
Button("From Beginning", systemImage: "play.fill") {
manager.proxy?.setSeconds(.zero)
manager.setPlaybackRequestStatus(status: .playing)
containerState.select(supplement: nil)
}
#if os(iOS)
.buttonStyle(.material)
#endif
.frame(width: 200, height: 50)
.font(.subheadline)
.fontWeight(.semibold)
}
// TODO: may need to be a layout for correct overview frame
// with scrolling if too long
var iOSView: some View {
CompactOrRegularView(
isCompact: containerState.isCompact
) {
iOSCompactView
} regularView: {
iOSRegularView
}
.padding(.leading, safeAreaInsets.leading)
.padding(.trailing, safeAreaInsets.trailing)
.edgePadding(.horizontal)
.edgePadding(.bottom)
}
@ViewBuilder
private var iOSCompactView: some View {
VStack(alignment: .leading) {
Group {
Text(item.displayTitle)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.leading)
if let overview = item.overview {
Text(overview)
.font(.subheadline)
.fontWeight(.regular)
}
accessoryView
.font(.caption)
.foregroundStyle(.secondary)
}
.allowsHitTesting(false)
if !item.isLiveStream {
Button {
manager.proxy?.setSeconds(.zero)
manager.setPlaybackRequestStatus(status: .playing)
containerState.select(supplement: nil)
} label: {
ZStack {
RoundedRectangle(cornerRadius: 7)
.foregroundStyle(.white)
Label("From Beginning", systemImage: "play.fill")
.fontWeight(.semibold)
.foregroundStyle(.black)
}
}
.frame(maxWidth: .infinity)
.frame(height: 40)
}
}
.frame(maxWidth: .infinity, alignment: .topLeading)
}
@ViewBuilder
private var iOSRegularView: some View {
HStack(alignment: .bottom, spacing: EdgeInsets.edgePadding) {
// TODO: determine what to do with non-portrait (channel, home video) images
// - use aspect ratio?
PosterImage(
item: item,
type: item.preferredPosterDisplayType,
contentMode: .fit
)
.environment(\.isOverComplexContent, true)
VStack(alignment: .leading, spacing: 5) {
Text(item.displayTitle)
.font(.callout)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.leading)
if let overview = item.overview {
Text(overview)
.font(.subheadline)
.fontWeight(.regular)
.lineLimit(3)
}
accessoryView
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
if !item.isLiveStream {
VStack {
fromBeginningButton
}
}
}
}
var tvOSView: some View { EmptyView() }
}
}